Файловый менеджер - Редактировать - /var/www/xthruster/html/wp-content/uploads/flags/json-endpoints.tar
Назад
class.wpcom-json-api-get-customcss.php 0000644 00000003524 14722054026 0014030 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Custom Css endpoint * * Endpoint: https://public-api.wordpress.com/rest/v1.1/sites/$site/customcss/ */ new WPCOM_JSON_API_Get_CustomCss_Endpoint( array( 'description' => 'Retrieve custom-css data for a site.', 'group' => '__do_not_document', 'stat' => 'customcss:1:get', 'method' => 'GET', 'min_version' => '1.1', 'path' => '/sites/%s/customcss', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'response_format' => array( 'css' => '(string) The raw CSS.', 'preprocessor' => '(string) The name of the preprocessor if any.', 'add_to_existing' => '(bool) False to skip the existing styles.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/customcss', 'example_response' => ' { "css": ".site-title { color: #fff; }", "preprocessor": "sass", "add_to_existing": "true" }', ) ); /** * GET Custom CSS Endpoint */ class WPCOM_JSON_API_Get_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { // Switch to the given blog. $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = array( 'css' => Jetpack_Custom_CSS::get_css(), 'preprocessor' => Jetpack_Custom_CSS::get_preprocessor_key(), 'add_to_existing' => ! Jetpack_Custom_CSS::skip_stylesheet(), ); $defaults = array( 'css' => '', 'preprocessor' => '', 'add_to_existing' => true, ); return wp_parse_args( $args, $defaults ); } } class.wpcom-json-api-update-term-endpoint.php 0000644 00000022616 14722054026 0015300 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update site terms API endpoints. * * Endpoints: * Create a new term: /sites/%s/taxonomies/%s/terms/new * Edit a term: /sites/%s/taxonomies/%s/terms/slug:%s * Delete a term: /sites/%s/taxonomies/%s/terms/slug:%s/delete */ new WPCOM_JSON_API_Update_Term_Endpoint( array( 'description' => 'Create a new term.', 'group' => 'taxonomy', 'stat' => 'terms:new', 'method' => 'POST', 'path' => '/sites/%s/taxonomies/%s/terms/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$taxonomy' => '(string) Taxonomy', ), 'request_format' => array( 'name' => '(string) Name of the term', 'description' => '(string) A description of the term', 'parent' => '(int) The parent ID for the term, if hierarchical', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/new', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'name' => 'Ribs & Chicken', ), ), ) ); new WPCOM_JSON_API_Update_Term_Endpoint( array( 'description' => 'Edit a term.', 'group' => 'taxonomy', 'stat' => 'terms:1:POST', 'method' => 'POST', 'path' => '/sites/%s/taxonomies/%s/terms/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$taxonomy' => '(string) Taxonomy', '$slug' => '(string) The term slug', ), 'request_format' => array( 'name' => '(string) Name of the term', 'description' => '(string) A description of the term', 'parent' => '(int) The parent ID for the term, if hierarchical', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/slug:testing-term', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'description' => 'The most delicious', ), ), ) ); new WPCOM_JSON_API_Update_Term_Endpoint( array( 'description' => 'Delete a term.', 'group' => 'taxonomy', 'stat' => 'terms:1:delete', 'method' => 'POST', 'path' => '/sites/%s/taxonomies/%s/terms/slug:%s/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$taxonomy' => '(string) Taxonomy', '$slug' => '(string) The term slug', ), 'response_format' => array( 'slug' => '(string) The slug of the deleted term', 'success' => '(bool) Whether the operation was successful', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/slug:$term/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Update site terms API endpoint class. */ class WPCOM_JSON_API_Update_Term_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint { /** * Update site terms API callback. * * - /sites/%s/taxonomies/%s/terms/new -> $blog_id, $taxonomy * - /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug * - /sites/%s/taxonomies/%s/terms/slug:%s/delete -> $blog_id, $taxonomy, $slug * * @param string $path API path. * @param int $blog_id Blog ID. * @param string $taxonomy Taxonomy. * @param int|string $slug Slug, term name. */ public function callback( $path = '', $blog_id = 0, $taxonomy = 'category', $slug = 0 ) { $slug = urldecode( $slug ); $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } $user = wp_get_current_user(); if ( ! $user || is_wp_error( $user ) || ! $user->ID ) { return new WP_Error( 'authorization_required', 'An active access token must be used to manage taxonomies.', 403 ); } $taxonomy_meta = get_taxonomy( $taxonomy ); if ( false === $taxonomy_meta || ( ! $taxonomy_meta->public && ! current_user_can( $taxonomy_meta->cap->manage_terms ) && ! current_user_can( $taxonomy_meta->cap->edit_terms ) && ! current_user_can( $taxonomy_meta->cap->delete_terms ) ) ) { return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 ); } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_term( $path, $blog_id, $slug, $taxonomy ); } elseif ( $this->api->ends_with( $path, '/new' ) ) { return $this->new_term( $path, $blog_id, $taxonomy ); } return $this->update_term( $path, $blog_id, $slug, $taxonomy ); } /** * Create a new term. * * - /sites/%s/taxonomies/%s/terms/new -> $blog_id, $taxonomy * * @param string $path API path. * @param int $blog_id Blog ID. * @param string $taxonomy Taxonomy. */ public function new_term( $path, $blog_id, $taxonomy ) { $args = $this->query_args(); $input = $this->input(); if ( ! is_array( $input ) || ! $input || ! strlen( $input['name'] ) ) { return new WP_Error( 'invalid_input', 'Unknown data passed', 400 ); } $tax = get_taxonomy( $taxonomy ); if ( ! current_user_can( $tax->cap->manage_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } if ( ! isset( $input['parent'] ) || ! is_taxonomy_hierarchical( $taxonomy ) ) { $input['parent'] = 0; } $term = get_term_by( 'name', $input['name'], $taxonomy ); if ( $term ) { // the same name is allowed as long as the parents are different. if ( $input['parent'] === $term->parent ) { return new WP_Error( 'duplicate', 'A taxonomy with that name already exists', 409 ); } } $data = wp_insert_term( addslashes( $input['name'] ), $taxonomy, array( 'description' => isset( $input['description'] ) ? addslashes( $input['description'] ) : '', 'parent' => $input['parent'], ) ); if ( is_wp_error( $data ) ) { return $data; } $term = get_term_by( 'id', $data['term_id'], $taxonomy ); $return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'terms' ); return $return; } /** * Update a term. * * - /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug * * @param string $path API path. * @param int $blog_id Blog ID. * @param int|string $slug Slug, term name. * @param string $taxonomy Taxonomy. */ public function update_term( $path, $blog_id, $slug, $taxonomy ) { $tax = get_taxonomy( $taxonomy ); if ( ! current_user_can( $tax->cap->edit_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } $term = get_term_by( 'slug', $slug, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); } $args = $this->query_args(); $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $update = array(); if ( ! empty( $input['parent'] ) || is_taxonomy_hierarchical( $taxonomy ) ) { $update['parent'] = $input['parent']; } if ( isset( $input['description'] ) ) { $update['description'] = addslashes( $input['description'] ); } if ( ! empty( $input['name'] ) ) { $update['name'] = addslashes( $input['name'] ); } $data = wp_update_term( $term->term_id, $taxonomy, $update ); if ( is_wp_error( $data ) ) { return $data; } $term = get_term_by( 'id', $data['term_id'], $taxonomy ); $return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'terms' ); return $return; } /** * Delete a term. * * - /sites/%s/taxonomies/%s/terms/slug:%s/delete -> $blog_id, $taxonomy, $slug * * @param string $path API path. * @param int $blog_id Blog ID. * @param int|string $slug Slug, term name. * @param string $taxonomy Taxonomy. */ public function delete_term( $path, $blog_id, $slug, $taxonomy ) { $term = get_term_by( 'slug', $slug, $taxonomy ); $tax = get_taxonomy( $taxonomy ); if ( ! current_user_can( $tax->cap->delete_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } if ( ! $term || is_wp_error( $term ) ) { return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); } $args = $this->query_args(); $return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'terms' ); wp_delete_term( $term->term_id, $taxonomy ); return array( 'slug' => (string) $term->slug, 'success' => true, ); } } class.wpcom-json-api-get-site-endpoint.php 0000644 00000107224 14722054026 0014571 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_GET_Site_Endpoint( array( 'description' => 'Get information about a site.', 'group' => 'sites', 'stat' => 'sites:X', 'allowed_if_flagged' => true, 'method' => 'GET', 'max_version' => '1.1', 'new_version' => '1.2', 'path' => '/sites/%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_jetpack_site_auth' => true, 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'context' => false, 'options' => '(string) Optional. Returns specified options only. Comma-separated list. Example: options=login_url,timezone', ), 'response_format' => WPCOM_JSON_API_GET_Site_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/', ) ); /** * GET Site endpoint class. */ class WPCOM_JSON_API_GET_Site_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Site meta data. * * @var array $site_format */ public static $site_format = array( 'ID' => '(int) Site ID', 'name' => '(string) Title of site', 'description' => '(string) Tagline or description of site', 'URL' => '(string) Full URL to the site', 'user_can_manage' => '(bool) The current user can manage this site', // deprecated. 'capabilities' => '(array) Array of capabilities for the current user on this site.', 'jetpack' => '(bool) Whether the site is a Jetpack site or not', 'jetpack_connection' => '(bool) Whether the site is connected to WP.com via `jetpack-connection`', 'is_multisite' => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.', 'site_owner' => '(int) User ID of the site owner', 'post_count' => '(int) The number of posts the site has', 'subscribers_count' => '(int) The number of subscribers the site has', 'lang' => '(string) Primary language code of the site', 'icon' => '(array) An array of icon formats for the site', 'logo' => '(array) The site logo, set in the Customizer', 'visible' => '(bool) If this site is visible in the user\'s site list', 'is_private' => '(bool) If the site is a private site or not', 'is_coming_soon' => '(bool) If the site is marked as "coming soon" or not', 'single_user_site' => '(bool) Whether the site is single user. Only returned for WP.com sites and for Jetpack sites with version 3.4 or higher.', 'is_vip' => '(bool) If the site is a VIP site or not.', 'is_following' => '(bool) If the current user is subscribed to this site in the reader', 'organization_id' => '(int) P2 Organization identifier.', 'options' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site. Note: Post formats is deprecated, please see /sites/$id/post-formats/', 'p2_thumbnail_elements' => '(array) Details used to render a thumbnail of the site. P2020 themed sites only.', 'plan' => '(array) Details of the current plan for this site.', 'products' => '(array) Details of the current products for this site.', 'zendesk_site_meta' => '(array) Site meta data for Zendesk.', 'updates' => '(array) An array of available updates for plugins, themes, wordpress, and languages.', 'jetpack_modules' => '(array) A list of active Jetpack modules.', 'meta' => '(object) Meta data', 'quota' => '(array) An array describing how much space a user has left for uploads', 'launch_status' => '(string) A string describing the launch status of a site', 'site_migration' => '(array) Data about any migration into the site.', 'is_fse_active' => '(bool) If the site has Full Site Editing active or not.', 'is_fse_eligible' => '(bool) If the site is capable of Full Site Editing or not', 'is_core_site_editor_enabled' => '(bool) If the site has the core site editor enabled.', 'is_wpcom_atomic' => '(bool) If the site is a WP.com Atomic one.', 'is_wpcom_staging_site' => '(bool) If the site is a WP.com staging site.', 'user_interactions' => '(array) An array of user interactions with a site.', 'was_ecommerce_trial' => '(bool) If the site ever used an eCommerce trial.', 'was_upgraded_from_trial' => '(bool) If the site ever upgraded to a paid plan from a trial.', 'was_migration_trial' => '(bool) If the site ever used a migration trial.', 'was_hosting_trial' => '(bool) If the site ever used a hosting trial.', 'wpcom_site_setup' => '(string) The WP.com site setup identifier.', 'is_deleted' => '(bool) If the site flagged as deleted.', 'is_a4a_client' => '(bool) If the site is an A4A client site.', 'is_a4a_dev_site' => '(bool) If the site is an A4A dev site.', ); /** * No member fields. * * @var array $no_member_fields */ protected static $no_member_fields = array( 'ID', 'name', 'description', 'URL', 'jetpack', 'jetpack_connection', 'post_count', 'subscribers_count', 'lang', 'locale', 'icon', 'logo', 'visible', 'is_private', 'is_coming_soon', 'is_following', 'organization_id', 'meta', 'launch_status', 'site_migration', 'is_fse_active', 'is_fse_eligible', 'is_core_site_editor_enabled', 'is_wpcom_atomic', 'is_wpcom_staging_site', 'is_deleted', 'is_a4a_client', 'is_a4a_dev_site', ); /** * Site options. * * @var array $site_options_format */ protected static $site_options_format = array( 'timezone', 'gmt_offset', 'blog_public', 'videopress_enabled', 'upgraded_filetypes_enabled', 'login_url', 'admin_url', 'is_mapped_domain', 'is_redirect', 'unmapped_url', 'featured_images_enabled', 'theme_slug', 'theme_errors', 'header_image', 'background_color', 'image_default_link_type', 'image_thumbnail_width', 'image_thumbnail_height', 'image_thumbnail_crop', 'image_medium_width', 'image_medium_height', 'image_large_width', 'image_large_height', 'permalink_structure', 'post_formats', 'default_post_format', 'default_category', 'allowed_file_types', 'show_on_front', /** This filter is documented in modules/likes.php */ 'default_likes_enabled', 'default_sharing_status', 'default_comment_status', 'default_ping_status', 'software_version', 'created_at', 'updated_at', 'wordads', 'publicize_permanently_disabled', 'frame_nonce', 'jetpack_frame_nonce', 'page_on_front', 'page_for_posts', 'headstart', 'headstart_is_fresh', 'ak_vp_bundle_enabled', Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION, Jetpack_SEO_Titles::TITLE_FORMATS_OPTION, 'verification_services_codes', 'podcasting_archive', 'is_domain_only', 'is_automated_transfer', 'is_wpcom_atomic', 'is_wpcom_store', 'signup_is_store', 'has_pending_automated_transfer', 'woocommerce_is_active', 'editing_toolkit_is_active', 'design_type', 'site_goals', 'site_segment', 'site_source_slug', 'import_engine', 'is_pending_plan', 'is_wpforteams_site', 'p2_hub_blog_id', 'site_creation_flow', 'is_cloud_eligible', 'selected_features', 'anchor_podcast', 'was_created_with_blank_canvas_design', 'videopress_storage_used', 'is_difm_lite_in_progress', 'site_intent', 'site_partner_bundle', 'site_goals', 'onboarding_segment', 'site_vertical_id', 'blogging_prompts_settings', 'launchpad_screen', 'launchpad_checklist_tasks_statuses', 'wpcom_production_blog_id', 'wpcom_staging_blog_ids', 'can_blaze', 'wpcom_site_setup', 'is_commercial', 'is_commercial_reasons', 'wpcom_admin_interface', 'wpcom_classic_early_release', ); /** * Jetpack response fields. * * @var array $jetpack_response_field_additions */ protected static $jetpack_response_field_additions = array( 'subscribers_count', 'site_migration', 'site_owner', 'is_wpcom_staging_site', 'was_ecommerce_trial', 'was_migration_trial', 'was_hosting_trial', 'was_upgraded_from_trial', 'is_a4a_dev_site', ); /** * Jetpack response field member additions. * * @var array $jetpack_response_field_member_additions */ protected static $jetpack_response_field_member_additions = array( 'capabilities', 'plan', 'products', 'zendesk_site_meta', ); /** * Jetpack response option additions. * * @var array $jetpack_response_field_member_additions */ protected static $jetpack_response_option_additions = array( 'publicize_permanently_disabled', 'ak_vp_bundle_enabled', 'is_automated_transfer', 'is_wpcom_atomic', 'is_wpcom_store', 'woocommerce_is_active', 'editing_toolkit_is_active', 'frame_nonce', 'jetpack_frame_nonce', 'design_type', 'wordads', // Use the site registered date from wpcom, since it is only available in a multisite context // and defaults to `0000-00-00T00:00:00+00:00` from the Jetpack site. // See https://github.com/Automattic/jetpack/blob/58638f46094b36f5df9cbc4570006544f0ad300c/sal/class.json-api-site-base.php#L387. 'created_at', 'updated_at', 'is_pending_plan', 'is_cloud_eligible', 'videopress_storage_used', 'blogging_prompts_settings', 'wpcom_production_blog_id', 'wpcom_staging_blog_ids', 'is_commercial', 'is_commercial_reasons', 'wpcom_admin_interface', 'wpcom_classic_early_release', ); /** * Current enabled trials. * * @var array $jetpack_enabled_trials */ public static $jetpack_enabled_trials = array( 'was_ecommerce_trial' => 'ecommerce', 'was_migration_trial' => 'migration', 'was_hosting_trial' => 'hosting', ); /** * Site * * @var SAL_Site $site. */ private $site; /** * Fields to include. * * @var $fields_to_include */ protected $fields_to_include = '_all'; /** * Options to include. * * @var $options_to_include */ protected $options_to_include = '_all'; /** * * API callback. * * /sites/mine * /sites/%s -> $blog_id\ * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { if ( 'mine' === $blog_id ) { $api = WPCOM_JSON_API::init(); if ( ! $api->token_details || empty( $api->token_details['blog_id'] ) ) { return new WP_Error( 'authorization_required', 'An active access token must be used to query information about the current blog.', 403 ); } $blog_id = $api->token_details['blog_id']; } add_filter( 'wpcom_allow_jetpack_blog_token', '__return_true' ); $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $this->filter_fields_and_options(); $response = $this->build_current_site_response(); /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'sites' ); return $response; } /** * Filter fields and options. */ public function filter_fields_and_options() { $query_args = $this->query_args(); $this->fields_to_include = empty( $query_args['fields'] ) ? '_all' : array_map( 'trim', explode( ',', $query_args['fields'] ) ); $this->options_to_include = empty( $query_args['options'] ) ? '_all' : array_map( 'trim', explode( ',', $query_args['options'] ) ); } /** * Collects the necessary information to return for a site's response. * * @return array */ public function build_current_site_response() { $blog_id = (int) $this->api->get_blog_id_for_output(); $this->site = $this->get_platform()->get_site( $blog_id ); /** * Filter the structure of information about the site to return. * * @module json-api * * @since 3.9.3 * * @param array $site_format Data structure. */ $default_fields = array_keys( apply_filters( 'sites_site_format', self::$site_format ) ); $response_keys = is_array( $this->fields_to_include ) ? array_intersect( $default_fields, $this->fields_to_include ) : $default_fields; $has_blog_access = $this->has_blog_access( $this->api->token_details ); $has_user_access = $this->has_user_access(); if ( ! $has_user_access && ! $has_blog_access ) { // Public access without user or blog auth, only return `$no_member_fields`. $response_keys = array_intersect( $response_keys, self::$no_member_fields ); } elseif ( $has_user_access && ! current_user_can( 'edit_posts' ) ) { // Subscriber level user, don't return site options. $response_keys = array_diff( $response_keys, array( 'options' ) ); } return $this->render_response_keys( $response_keys ); } /** * Checks that the current user has access to the current blog. * * @return bool Whether or not the current user can access the current blog. */ private function has_user_access() { return is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ); } /** * Checks if the request has a valid blog token for the current blog. * * @param array $token_details Access token for the api request. * @return bool */ private function has_blog_access( $token_details ) { $token_details = (array) $token_details; if ( ! isset( $token_details['access'] ) || ! isset( $token_details['auth'] ) || ! isset( $token_details['blog_id'] ) ) { return false; } return 'jetpack' === $token_details['auth'] && 'blog' === $token_details['access'] && get_current_blog_id() === $token_details['blog_id']; } /** * Render response keys. * * @param array $response_keys - the response keys. */ private function render_response_keys( &$response_keys ) { $response = array(); $is_user_logged_in = is_user_logged_in(); $this->site->before_render(); foreach ( $response_keys as $key ) { $this->render_response_key( $key, $response, $is_user_logged_in ); } $this->site->after_render( $response ); return $response; } /** * Render response key. * * @param string $key - the key. * @param array $response - the response. * @param boolean $is_user_logged_in - if the user is logged in. */ protected function render_response_key( $key, &$response, $is_user_logged_in ) { do_action( 'pre_render_site_response_key', $key ); switch ( $key ) { case 'ID': $response[ $key ] = $this->site->blog_id; break; case 'name': $response[ $key ] = $this->site->get_name(); break; case 'description': $response[ $key ] = $this->site->get_description(); break; case 'URL': $response[ $key ] = $this->site->get_url(); break; case 'user_can_manage': $response[ $key ] = $this->site->user_can_manage(); // fall through is intentional. case 'is_private': $response[ $key ] = $this->site->is_private(); break; case 'is_coming_soon': // This option is stored on wp.com for both simple and atomic sites. @see mu-plugins/private-blog.php. $response[ $key ] = $this->site->is_coming_soon(); break; case 'launch_status': $response[ $key ] = $this->site->get_launch_status(); break; case 'visible': $response[ $key ] = $this->site->is_visible(); break; case 'subscribers_count': $response[ $key ] = $this->site->get_subscribers_count(); break; case 'post_count': if ( $is_user_logged_in ) { $response[ $key ] = $this->site->get_post_count(); } break; case 'icon': $icon = $this->site->get_icon(); if ( $icon !== null ) { $response[ $key ] = $icon; } break; case 'logo': $response[ $key ] = $this->site->get_logo(); break; case 'is_following': $response[ $key ] = $this->site->is_following(); break; case 'options': // small optimisation - don't recalculate. $all_options = apply_filters( 'sites_site_options_format', self::$site_options_format ); $options_response_keys = is_array( $this->options_to_include ) ? array_intersect( $all_options, $this->options_to_include ) : $all_options; $options = $this->render_option_keys( $options_response_keys ); $this->site->after_render_options( $options ); $response[ $key ] = (object) $options; break; case 'meta': $this->build_meta_response( $response ); break; case 'lang': $response[ $key ] = $is_user_logged_in ? $this->site->get_locale() : false; break; case 'locale': $response[ $key ] = $is_user_logged_in ? $this->site->get_locale() : false; break; case 'jetpack': $response[ $key ] = $this->site->is_jetpack(); break; case 'jetpack_connection': $response[ $key ] = $this->site->is_jetpack_connection(); break; case 'single_user_site': $response[ $key ] = $this->site->is_single_user_site(); break; case 'is_vip': $response[ $key ] = $this->site->is_vip(); break; case 'is_multisite': $response[ $key ] = $this->site->is_multisite(); break; case 'site_owner': $response[ $key ] = $this->site->get_site_owner(); break; case 'organization_id': $response[ $key ] = $this->site->get_p2_organization_id(); break; case 'capabilities': $response[ $key ] = $this->site->get_capabilities(); break; case 'jetpack_modules': if ( is_user_member_of_blog() ) { $response[ $key ] = $this->site->get_jetpack_modules(); } break; case 'plan': $response[ $key ] = $this->site->get_plan(); break; case 'products': $response[ $key ] = $this->site->get_products(); break; case 'zendesk_site_meta': $response[ $key ] = $this->site->get_zendesk_site_meta(); break; case 'quota': $response[ $key ] = $this->site->get_quota(); break; case 'site_migration': $response[ $key ] = $this->site->get_migration_meta(); break; case 'is_fse_active': $response[ $key ] = $this->site->is_fse_active(); break; case 'is_fse_eligible': $response[ $key ] = $this->site->is_fse_eligible(); break; case 'is_core_site_editor_enabled': $response[ $key ] = $this->site->is_core_site_editor_enabled(); break; case 'is_wpcom_atomic': $response[ $key ] = $this->site->is_wpcom_atomic(); break; case 'is_wpcom_staging_site': $response[ $key ] = $this->site->is_wpcom_staging_site(); break; case 'user_interactions': $response[ $key ] = $this->site->get_user_interactions(); break; case 'p2_thumbnail_elements': $response[ $key ] = $this->site->get_p2_thumbnail_elements(); break; case 'was_ecommerce_trial': $response[ $key ] = $this->site->was_trial( self::$jetpack_enabled_trials['was_ecommerce_trial'] ); break; case 'was_migration_trial': $response[ $key ] = $this->site->was_trial( self::$jetpack_enabled_trials['was_migration_trial'] ); break; case 'was_hosting_trial': $response[ $key ] = $this->site->was_trial( self::$jetpack_enabled_trials['was_hosting_trial'] ); break; case 'was_upgraded_from_trial': $response[ $key ] = $this->site->was_upgraded_from_trial(); break; case 'is_deleted': $response[ $key ] = $this->site->is_deleted(); break; case 'is_a4a_client': $response[ $key ] = $this->site->is_a4a_client(); break; case 'is_a4a_dev_site': $response[ $key ] = $this->site->is_a4a_dev_site(); break; } do_action( 'post_render_site_response_key', $key ); } /** * Render option keys. * * @param array $options_response_keys - the response keys. */ protected function render_option_keys( &$options_response_keys ) { $options = array(); $site = $this->site; $custom_front_page = $site->is_custom_front_page(); foreach ( $options_response_keys as $key ) { switch ( $key ) { case 'timezone': $options[ $key ] = $site->get_timezone(); break; case 'gmt_offset': $options[ $key ] = $site->get_gmt_offset(); break; case 'videopress_enabled': $options[ $key ] = $site->has_videopress(); break; case 'upgraded_filetypes_enabled': $options[ $key ] = $site->upgraded_filetypes_enabled(); break; case 'login_url': $options[ $key ] = $site->get_login_url(); break; case 'admin_url': $options[ $key ] = $site->get_admin_url(); break; case 'is_mapped_domain': $options[ $key ] = $site->is_mapped_domain(); break; case 'is_redirect': $options[ $key ] = $site->is_redirect(); break; case 'unmapped_url': $options[ $key ] = $site->get_unmapped_url(); break; case 'featured_images_enabled': $options[ $key ] = $site->featured_images_enabled(); break; case 'theme_slug': $options[ $key ] = $site->get_theme_slug(); break; case 'theme_errors': $options[ $key ] = $site->get_theme_errors(); break; case 'header_image': $options[ $key ] = $site->get_header_image(); break; case 'background_color': $options[ $key ] = $site->get_background_color(); break; case 'image_default_link_type': $options[ $key ] = $site->get_image_default_link_type(); break; case 'image_thumbnail_width': $options[ $key ] = $site->get_image_thumbnail_width(); break; case 'image_thumbnail_height': $options[ $key ] = $site->get_image_thumbnail_height(); break; case 'image_thumbnail_crop': $options[ $key ] = $site->get_image_thumbnail_crop(); break; case 'image_medium_width': $options[ $key ] = $site->get_image_medium_width(); break; case 'image_medium_height': $options[ $key ] = $site->get_image_medium_height(); break; case 'image_large_width': $options[ $key ] = $site->get_image_large_width(); break; case 'image_large_height': $options[ $key ] = $site->get_image_large_height(); break; case 'permalink_structure': $options[ $key ] = $site->get_permalink_structure(); break; case 'post_formats': $options[ $key ] = $site->get_post_formats(); break; case 'default_post_format': $options[ $key ] = $site->get_default_post_format(); break; case 'default_category': $options[ $key ] = $site->get_default_category(); break; case 'allowed_file_types': $options[ $key ] = $site->allowed_file_types(); break; case 'show_on_front': $options[ $key ] = $site->get_show_on_front(); break; /** This filter is documented in modules/likes.php */ case 'default_likes_enabled': $options[ $key ] = $site->get_default_likes_enabled(); break; case 'default_sharing_status': $options[ $key ] = $site->get_default_sharing_status(); break; case 'default_comment_status': $options[ $key ] = $site->get_default_comment_status(); break; case 'default_ping_status': $options[ $key ] = $site->default_ping_status(); break; case 'software_version': $options[ $key ] = $site->get_wordpress_version(); break; case 'created_at': $options[ $key ] = $site->get_registered_date(); break; case 'updated_at': $options[ $key ] = $site->get_last_update_date(); break; case 'wordads': $options[ $key ] = $site->has_wordads(); break; case 'publicize_permanently_disabled': $options[ $key ] = $site->is_publicize_permanently_disabled(); break; case 'frame_nonce': $options[ $key ] = $site->get_frame_nonce(); break; case 'jetpack_frame_nonce': $options[ $key ] = $site->get_jetpack_frame_nonce(); break; case 'page_on_front': if ( $custom_front_page ) { $options[ $key ] = $site->get_page_on_front(); } break; case 'page_for_posts': if ( $custom_front_page ) { $options[ $key ] = $site->get_page_for_posts(); } break; case 'headstart': $options[ $key ] = $site->is_headstart(); break; case 'headstart_is_fresh': $options[ $key ] = $site->is_headstart_fresh(); break; case 'ak_vp_bundle_enabled': $options[ $key ] = $site->get_ak_vp_bundle_enabled(); break; case Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION: $options[ $key ] = $site->get_jetpack_seo_front_page_description(); break; case Jetpack_SEO_Titles::TITLE_FORMATS_OPTION: $options[ $key ] = $site->get_jetpack_seo_title_formats(); break; case 'verification_services_codes': $options[ $key ] = $site->get_verification_services_codes(); break; case 'podcasting_archive': $options[ $key ] = $site->get_podcasting_archive(); break; case 'is_domain_only': $options[ $key ] = $site->is_domain_only(); break; case 'is_automated_transfer': $options[ $key ] = $site->is_automated_transfer(); break; case 'blog_public': $options[ $key ] = $site->get_blog_public(); break; case 'is_wpcom_atomic': $options[ $key ] = $site->is_wpcom_atomic(); break; case 'is_wpcom_store': $options[ $key ] = $site->is_wpcom_store(); break; case 'signup_is_store': $signup_is_store = $site->signup_is_store(); if ( $signup_is_store ) { $options[ $key ] = $site->signup_is_store(); } break; case 'has_pending_automated_transfer': $has_pending_automated_transfer = $site->has_pending_automated_transfer(); if ( $has_pending_automated_transfer ) { $options[ $key ] = true; } break; case 'woocommerce_is_active': $options[ $key ] = $site->woocommerce_is_active(); break; case 'editing_toolkit_is_active': $options[ $key ] = $site->editing_toolkit_is_active(); break; case 'design_type': $options[ $key ] = $site->get_design_type(); break; case 'site_segment': $options[ $key ] = $site->get_site_segment(); break; case 'import_engine': $options[ $key ] = $site->get_import_engine(); break; case 'is_pending_plan': $options[ $key ] = $site->is_pending_plan(); break; case 'is_wpforteams_site': $options[ $key ] = $site->is_wpforteams_site(); break; case 'p2_hub_blog_id': $options[ $key ] = $site->get_p2_hub_blog_id(); break; case 'site_creation_flow': $site_creation_flow = $site->get_site_creation_flow(); if ( $site_creation_flow ) { $options[ $key ] = $site_creation_flow; } break; case 'site_source_slug': $site_source_slug = $site->get_site_source_slug(); if ( $site_source_slug ) { $options[ $key ] = $site_source_slug; } break; case 'is_cloud_eligible': $options[ $key ] = $site->is_cloud_eligible(); break; case 'selected_features': $selected_features = $site->get_selected_features(); if ( $selected_features ) { $options[ $key ] = $selected_features; } break; case 'anchor_podcast': $options[ $key ] = $site->get_anchor_podcast(); break; case 'was_created_with_blank_canvas_design': $options[ $key ] = $site->was_created_with_blank_canvas_design(); break; case 'videopress_storage_used': $options[ $key ] = $this->site->get_videopress_storage_used(); break; case 'is_difm_lite_in_progress': $options[ $key ] = $site->is_difm_lite_in_progress(); break; case 'site_intent': $options[ $key ] = $site->get_site_intent(); break; case 'site_partner_bundle': $options[ $key ] = $site->get_site_partner_bundle(); break; case 'site_goals': $options[ $key ] = $site->get_site_goals(); break; case 'onboarding_segment': $options[ $key ] = $site->get_onboarding_segment(); break; case 'site_vertical_id': $options[ $key ] = $site->get_site_vertical_id(); break; case 'blogging_prompts_settings': if ( current_user_can( 'edit_posts' ) ) { $options[ $key ] = $site->get_blogging_prompts_settings( get_current_user_id(), $site->blog_id ); } break; case 'launchpad_screen': $options[ $key ] = $site->get_launchpad_screen(); break; case 'launchpad_checklist_tasks_statuses': $options[ $key ] = $site->get_launchpad_checklist_tasks_statuses(); break; case 'wpcom_production_blog_id': $options[ $key ] = $site->get_wpcom_production_blog_id(); break; case 'wpcom_staging_blog_ids': $options[ $key ] = $site->get_wpcom_staging_blog_ids(); break; case 'can_blaze': $options[ $key ] = $site->can_blaze(); break; case 'wpcom_site_setup': $options[ $key ] = $site->get_wpcom_site_setup(); break; case 'is_commercial': $options[ $key ] = $site->is_commercial(); break; case 'is_commercial_reasons': $options[ $key ] = $site->get_is_commercial_reasons(); break; case 'wpcom_admin_interface': $options[ $key ] = $site->get_wpcom_admin_interface(); break; case 'wpcom_classic_early_release': $options[ $key ] = $site->get_wpcom_classic_early_release(); break; } } return $options; } /** * Build meta response. * * @param array $response - the response. */ protected function build_meta_response( &$response ) { $links = array( 'self' => (string) $this->links->get_site_link( $this->site->blog_id ), 'help' => (string) $this->links->get_site_link( $this->site->blog_id, 'help' ), 'posts' => (string) $this->links->get_site_link( $this->site->blog_id, 'posts/' ), 'comments' => (string) $this->links->get_site_link( $this->site->blog_id, 'comments/' ), 'xmlrpc' => (string) $this->site->get_xmlrpc_url(), ); $icon = $this->site->get_icon(); if ( ! empty( $icon ) && ! empty( $icon['media_id'] ) ) { $links['site_icon'] = (string) $this->links->get_site_link( $this->site->blog_id, 'media/' . $icon['media_id'] ); } $response['meta'] = (object) array( 'links' => (object) $links, ); } /** * Apply any WPCOM-only response components to a Jetpack site response. * * @param array $response - the response. */ public function decorate_jetpack_response( &$response ) { $this->site = $this->get_platform()->get_site( $response->ID ); switch_to_blog( $this->site->get_id() ); $wpcom_response = $this->render_response_keys( self::$jetpack_response_field_additions ); foreach ( $wpcom_response as $key => $value ) { $response->{ $key } = $value; } if ( $this->has_user_access() || $this->has_blog_access( $this->api->token_details ) ) { $wpcom_member_response = $this->render_response_keys( self::$jetpack_response_field_member_additions ); foreach ( $wpcom_member_response as $key => $value ) { $response->{ $key } = $value; } } else { // ensure private data is not rendered for non members of the site. unset( $response->options ); unset( $response->is_vip ); unset( $response->single_user_site ); unset( $response->is_private ); unset( $response->is_coming_soon ); unset( $response->capabilities ); unset( $response->lang ); unset( $response->user_can_manage ); unset( $response->is_multisite ); unset( $response->site_owner ); unset( $response->plan ); unset( $response->products ); unset( $response->zendesk_site_meta ); } // render additional options. if ( isset( $response->options ) && $response->options ) { $wpcom_options_response = $this->render_option_keys( self::$jetpack_response_option_additions ); // Remove heic from jetpack (and atomic) sites so that the iOS app know to convert the file format into a JPEG. // heic fromat is currently not supported by for uploading. // See https://jetpackp2.wordpress.com/2020/08/19/image-uploads-in-the-wp-ios-app-broken if ( $this->site->is_jetpack() && isset( $response->options['allowed_file_types'] ) ) { $remove_file_types = array( 'heic', ); $response->options['allowed_file_types'] = array_values( array_diff( $response->options['allowed_file_types'], $remove_file_types ) ); } foreach ( $wpcom_options_response as $key => $value ) { $response->options[ $key ] = $value; } } restore_current_blog(); return $response; // possibly no need since it's modified in place. } } new WPCOM_JSON_API_List_Post_Formats_Endpoint( array( 'description' => 'Get a list of post formats supported by a site.', 'group' => '__do_not_document', 'stat' => 'sites:X:post-formats', 'method' => 'GET', 'path' => '/sites/%s/post-formats', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'allow_fallback_to_jetpack_blog_token' => true, 'response_format' => array( 'formats' => '(object) An object of supported post formats, each key a supported format slug mapped to its display string.', ), ) ); /** * List Post Formates endpoint class. */ class WPCOM_JSON_API_List_Post_Formats_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore /** * * API callback. * * /sites/%s/post-formats -> $blog_id * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } // Get a list of supported post formats. $all_formats = get_post_format_strings(); $supported = get_theme_support( 'post-formats' ); $response = array( 'formats' => array(), ); $supported_formats = $response['formats']; if ( isset( $supported[0] ) ) { foreach ( $supported[0] as $format ) { $supported_formats[ $format ] = $all_formats[ $format ]; } } $response['formats'] = (object) $supported_formats; return $response; } } new WPCOM_JSON_API_List_Page_Templates_Endpoint( array( 'description' => 'Get a list of page templates supported by a site.', 'group' => 'sites', 'stat' => 'sites:X:post-templates', 'method' => 'GET', 'path' => '/sites/%s/page-templates', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'response_format' => array( 'templates' => '(array) A list of supported page templates. Contains label and file.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/33534099/page-templates', ) ); /** * List page templates endpoint class. */ class WPCOM_JSON_API_List_Page_Templates_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore /** * * API callback. * /sites/%s/page-templates -> $blog_id * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } $response = array(); $page_templates = array(); $templates = get_page_templates(); ksort( $templates ); foreach ( array_keys( $templates ) as $label ) { $page_templates[] = array( 'label' => $label, 'file' => $templates[ $label ], ); } $response['templates'] = $page_templates; return $response; } } class.wpcom-json-api-list-terms-endpoint.php 0000644 00000011013 14722054026 0015141 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List terms endpoint. */ new WPCOM_JSON_API_List_Terms_Endpoint( array( 'description' => 'Get a list of a site\'s terms by taxonomy.', 'group' => 'taxonomy', 'stat' => 'terms', 'method' => 'GET', 'path' => '/sites/%s/taxonomies/%s/terms', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$taxonomy' => '(string) Taxonomy', ), 'query_parameters' => array( 'number' => '(int=100) The number of terms to return. Limit: 1000.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of terms. Takes precedence over the <code>offset</code> parameter.', 'search' => '(string) Limit response to include only terms whose names or slugs match the provided search query.', 'order' => array( 'ASC' => 'Return terms in ascending order.', 'DESC' => 'Return terms in descending order.', ), 'order_by' => array( 'name' => 'Order by the name of each tag.', 'count' => 'Order by the number of posts in each tag.', ), ), 'allow_fallback_to_jetpack_blog_token' => true, 'response_format' => array( 'found' => '(int) The number of terms returned.', 'terms' => '(array) Array of tag objects.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/taxonomies/post_tags/terms?number=5', ) ); /** * List terms endpoint class. * * /sites/%s/taxonomies/%s/terms -> $blog_id, $taxonomy */ class WPCOM_JSON_API_List_Terms_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. * @param string $taxonomy - the taxonomy. */ public function callback( $path = '', $blog_id = 0, $taxonomy = 'category' ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } $taxonomy_meta = get_taxonomy( $taxonomy ); if ( false === $taxonomy_meta || ( ! $taxonomy_meta->public && ! current_user_can( $taxonomy_meta->cap->assign_terms ) ) ) { return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 ); } $args = $this->query_args(); $args = $this->process_args( $args ); $formatted_terms = $this->get_formatted_terms( $taxonomy, $args ); if ( ! empty( $formatted_terms ) ) { /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'terms', is_countable( $formatted_terms ) ? count( $formatted_terms ) : 0 ); } return array( 'found' => (int) $this->get_found( $taxonomy, $args ), 'terms' => (array) $formatted_terms, ); } /** * Process args. * * @param array $args - the arguments. */ public function process_args( $args ) { $args['get'] = 'all'; if ( $args['number'] < 1 ) { $args['number'] = 100; } elseif ( 1000 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The number parameter must be less than or equal to 1000.', 400 ); } if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $args['offset'] = ( $args['page'] - 1 ) * $args['number']; unset( $args['page'] ); } if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $args['orderby'] = $args['order_by']; unset( $args['order_by'] ); unset( $args['context'], $args['pretty'], $args['http_envelope'], $args['fields'] ); return $args; } /** * Get found taxonomy term count. * * @param string $taxonomy - the taxonomy. * @param array $args - the arguments. */ public function get_found( $taxonomy, $args ) { unset( $args['offset'] ); $args['taxonomy'] = $taxonomy; return wp_count_terms( $args ); } /** * Format the taxonomy terms. * * @param string $taxonomy - the taxonomy. * @param array $args - the arguments. */ public function get_formatted_terms( $taxonomy, $args ) { $args['taxonomy'] = $taxonomy; $terms = get_terms( $args ); $formatted_terms = array(); foreach ( $terms as $term ) { $formatted_terms[] = $this->format_taxonomy( $term, $taxonomy, 'display' ); } return $formatted_terms; } } class.wpcom-json-api-get-comment-history-endpoint.php 0000644 00000004252 14722054026 0016763 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoint: /sites/%s/comment-history/%d */ new WPCOM_JSON_API_GET_Comment_History_Endpoint( array( 'description' => 'Get the audit history for given comment', 'group' => 'comments', 'stat' => 'comments:1:comment-history', 'method' => 'GET', 'path' => '/sites/%s/comment-history/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$comment_ID' => '(int) The comment ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comment-history/11', 'response_format' => array( 'comment_history' => '(array) Array of arrays representing the comment history objects.', ), ) ); /** * GET Comment History endpoint. */ class WPCOM_JSON_API_GET_Comment_History_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $comment_id - the comment ID. */ public function callback( $path = '', $blog_id = 0, $comment_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! get_current_user_id() ) { return new WP_Error( 'authorization_required', 'An active access token must be used to retrieve comment history.', 403 ); } if ( ! current_user_can_for_blog( $blog_id, 'edit_posts' ) ) { return new WP_Error( 'authorization_required', 'You are not authorized to view comment history on this blog.', 403 ); } if ( ! method_exists( 'Akismet', 'get_comment_history' ) ) { return new WP_Error( 'akismet_required', 'Akismet plugin must be active for this feature to work', 503 ); } $comment_history = Akismet::get_comment_history( $comment_id ); foreach ( $comment_history as &$item ) { // Times are stored as floating point values in microseconds. // We don't need that precision on the client so let's get rid of the decimal part. $item['time'] = (int) $item['time']; } return array( 'comment_history' => $comment_history ); } } class.wpcom-json-api-render-shortcode-endpoint.php 0000644 00000007144 14722054026 0016317 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Render_Shortcode_Endpoint( array( 'description' => 'Get a rendered shortcode for a site. Note: The current user must have publishing access.', 'group' => 'sites', 'stat' => 'shortcodes:render', 'method' => 'GET', 'path' => '/sites/%s/shortcodes/render', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'shortcode' => '(string) The query-string encoded shortcode string to render. Required. Only accepts one at a time.', ), 'response_format' => array( 'shortcode' => '(string) The shortcode that was passed in for rendering.', 'result' => '(html) The rendered HTML result of the shortcode.', 'scripts' => '(array) An array of JavaScript files needed to render the shortcode. Returned in the format of <code>{ "script-slug" : { "src": "http://example.com/file.js", "extra" : "" } }</code> where extra contains any neccessary extra JS for initializing the source file and src contains the script to load. Omitted if no scripts are neccessary.', 'styles' => '(array) An array of CSS files needed to render the shortcode. Returned in the format of <code>{ "style-slug" : { "src": "http://example.com/file.css", "media" : "all" } }</code>. Omitted if no styles are neccessary.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcodes/render?shortcode=%5Bgallery%20ids%3D%22729%2C732%2C731%2C720%22%5D', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Render shortcode endpoint class. * * /sites/%s/shortcodes/render -> $blog_id */ class WPCOM_JSON_API_Render_Shortcode_Endpoint extends WPCOM_JSON_API_Render_Endpoint { /** * The API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 ); } $args = $this->query_args(); $shortcode = trim( $args['shortcode'] ); // Quick validation - shortcodes should always be enclosed in brackets [] if ( ! wp_startswith( $shortcode, '[' ) || ! wp_endswith( $shortcode, ']' ) ) { return new WP_Error( 'invalid_shortcode', 'The shortcode parameter must begin and end with square brackets.', 400 ); } // Make sure only one shortcode is being rendered at a time $pattern = get_shortcode_regex(); preg_match_all( "/$pattern/s", $shortcode, $matches ); if ( is_countable( $matches[0] ) && count( $matches[0] ) > 1 ) { return new WP_Error( 'invalid_shortcode', 'Only one shortcode can be rendered at a time.', 400 ); } $render = $this->process_render( array( $this, 'do_shortcode' ), $shortcode ); // if nothing happened, then the shortcode does not exist. if ( $shortcode === $render['result'] ) { return new WP_Error( 'invalid_shortcode', 'The requested shortcode does not exist.', 400 ); } // our output for this endpoint.. $return = array(); $return['shortcode'] = $shortcode; $return['result'] = $render['result']; $return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] ); return $return; } } class.wpcom-json-api-update-post-endpoint.php 0000644 00000120172 14722054026 0015312 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update post endpoint. * * Endpoints: * Create a post: /sites/%s/posts/new * Update a post: /sites/%s/posts/%d * Delete a post: /sites/%s/posts/%d/delete * Restore a post: /sites/%s/posts/%d/restore */ new WPCOM_JSON_API_Update_Post_Endpoint( array( 'description' => 'Create a post.', 'group' => 'posts', 'stat' => 'posts:new', 'new_version' => '1.2', 'max_version' => '1', 'method' => 'POST', 'path' => '/sites/%s/posts/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( // explicitly document all input. 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'pending' => 'Mark the post as pending editorial approval.', 'auto-draft' => 'Save a placeholder for a newly created post, with no content.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'type' => "(string) The post type. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'categories' => '(array|string) Comma-separated list or array of categories (name or id)', 'tags' => '(array|string) Comma-separated list or array of tags (name or id)', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.', 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'comments_open' => "(bool) Should the post be open to comments? Defaults to the blog's preference.", 'pings_open' => "(bool) Should the post be open to comments? Defaults to the blog's preference.", 'likes_enabled' => "(bool) Should the post be open to likes? Defaults to the blog's preference.", 'sharing_enabled' => '(bool) Should sharing buttons show on this post? Defaults to true.', 'menu_order' => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World', 'content' => 'Hello. I am a test post. I was created by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); new WPCOM_JSON_API_Update_Post_Endpoint( array( 'description' => 'Edit a post.', 'group' => 'posts', 'stat' => 'posts:1:POST', 'new_version' => '1.2', 'max_version' => '1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'request_format' => array( 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'pending' => 'Mark the post as pending editorial approval.', 'trash' => 'Set the post as trashed.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'categories' => '(array|string) Comma-separated list or array of categories (name or id)', 'tags' => '(array|string) Comma-separated list or array of tags (name or id)', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'comments_open' => '(bool) Should the post be open to comments?', 'pings_open' => '(bool) Should the post be open to comments?', 'likes_enabled' => '(bool) Should the post be open to likes?', 'menu_order' => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.', 'sharing_enabled' => '(bool) Should sharing buttons show on this post?', 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options resposne of the site endpoint. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.', 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/881', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World (Again)', 'content' => 'Hello. I am an edited post. I was edited by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); new WPCOM_JSON_API_Update_Post_Endpoint( array( 'description' => 'Delete a post. Note: If the trash is enabled, this request will send the post to the trash. A second request will permanently delete the post.', 'group' => 'posts', 'stat' => 'posts:1:delete', 'new_version' => '1.1', 'max_version' => '1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/delete/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); new WPCOM_JSON_API_Update_Post_Endpoint( array( 'description' => 'Restore a post or page from the trash to its previous status.', 'group' => 'posts', 'stat' => 'posts:1:restore', 'method' => 'POST', 'new_version' => '1.1', 'max_version' => '1', 'path' => '/sites/%s/posts/%d/restore', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/restore/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Update post endpoint class. */ class WPCOM_JSON_API_Update_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint { /** * WPCOM_JSON_API_Update_Post_Endpoint constructor. * * @param array $args Args. */ public function __construct( $args ) { parent::__construct( $args ); if ( $this->api->ends_with( $this->path, '/delete' ) ) { $this->post_object_format['status']['deleted'] = 'The post has been deleted permanently.'; } } /** * Update post API callback. * * /sites/%s/posts/new -> $blog_id * /sites/%s/posts/%d -> $blog_id, $post_id * /sites/%s/posts/%d/delete -> $blog_id, $post_id * /sites/%s/posts/%d/restore -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. * * @return array|bool|WP_Error */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_post( $path, $blog_id, $post_id ); } elseif ( $this->api->ends_with( $path, '/restore' ) ) { return $this->restore_post( $path, $blog_id, $post_id ); } else { return $this->write_post( $path, $blog_id, $post_id ); } } /** * Create or update a post. * * /sites/%s/posts/new -> $blog_id * /sites/%s/posts/%d -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. */ public function write_post( $path, $blog_id, $post_id ) { $delete_featured_image = null; $new = $this->api->ends_with( $path, '/new' ); $args = $this->query_args(); // unhook publicize, it's hooked again later -- without this, skipping services is impossible. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { remove_action( 'save_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ), 100, 2 ); add_action( 'rest_api_inserted_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ) ); } if ( $new ) { $input = $this->input( true ); // default to post. if ( empty( $input['type'] ) ) { $input['type'] = 'post'; } if ( 'revision' === $input['type'] ) { if ( ! isset( $input['parent'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $input['status'] = 'inherit'; // force inherit for revision type. $input['slug'] = $input['parent'] . '-autosave-v1'; } elseif ( ! isset( $input['title'] ) && ! isset( $input['content'] ) && ! isset( $input['excerpt'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $post_type = get_post_type_object( $input['type'] ); if ( ! $this->is_post_type_allowed( $input['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } if ( ! empty( $input['author'] ) ) { $author_id = $this->parse_and_set_author( $input['author'], $input['type'] ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( 'publish' === $input['status'] ) { if ( ! current_user_can( $post_type->cap->publish_posts ) ) { if ( current_user_can( $post_type->cap->edit_posts ) ) { $input['status'] = 'pending'; } else { return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 ); } } } elseif ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 ); } } else { $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } if ( isset( $input['status'] ) && 'trash' === $input['status'] && ! current_user_can( 'delete_post', $post_id ) ) { return new WP_Error( 'unauthorized', 'User cannot delete post', 403 ); } $post = get_post( $post_id ); $_post_type = ( ! empty( $input['type'] ) ) ? $input['type'] : $post->post_type; $post_type = get_post_type_object( $_post_type ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } if ( ! empty( $input['author'] ) ) { $author_id = $this->parse_and_set_author( $input['author'], $_post_type ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( ( isset( $input['status'] ) && 'publish' === $input['status'] ) && 'publish' !== $post->post_status && ! current_user_can( 'publish_post', $post->ID ) ) { $input['status'] = 'pending'; } $last_status = $post->post_status; $new_status = isset( $input['status'] ) ? $input['status'] : $last_status; // Make sure that drafts get the current date when transitioning to publish if not supplied in the post. $date_in_past = ( strtotime( $post->post_date_gmt ) < time() ); if ( 'publish' === $new_status && 'draft' === $last_status && ! isset( $input['date_gmt'] ) && $date_in_past ) { $input['date_gmt'] = gmdate( 'Y-m-d H:i:s' ); } // Untrash a post so that the proper hooks get called as well as the comments get untrashed. if ( 'trash' === $last_status && 'trash' !== $new_status && isset( $post->ID ) ) { wp_untrash_post( $post->ID ); $untashed_post = get_post( $post->ID ); // Lets make sure that we use the revert the slug. if ( isset( $untashed_post->post_name ) && $untashed_post->post_name . '__trashed' === $input['slug'] ) { unset( $input['slug'] ); } } } if ( function_exists( 'wpcom_switch_to_locale' ) ) { // fixes calypso-pre-oss #12476: respect blog locale when creating the post slug. wpcom_switch_to_locale( get_blog_lang_code( $blog_id ) ); } // If date was set, $this->input will set date_gmt, date still needs to be adjusted for the blog's offset. if ( isset( $input['date_gmt'] ) ) { $gmt_offset = get_option( 'gmt_offset' ); $time_with_offset = strtotime( $input['date_gmt'] ) + $gmt_offset * HOUR_IN_SECONDS; $input['date'] = gmdate( 'Y-m-d H:i:s', $time_with_offset ); } if ( ! empty( $author_id ) && get_current_user_id() !== $author_id ) { if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'unauthorized', "User is not allowed to publish others' posts.", 403 ); } elseif ( ! user_can( $author_id, $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'Assigned author cannot publish post.', 403 ); } } if ( ! is_post_type_hierarchical( $post_type->name ) && 'revision' !== $post_type->name ) { unset( $input['parent'] ); } $tax_input = array(); foreach ( array( 'categories' => 'category', 'tags' => 'post_tag', ) as $key => $taxonomy ) { if ( ! isset( $input[ $key ] ) ) { continue; } $tax_input[ $taxonomy ] = array(); $is_hierarchical = is_taxonomy_hierarchical( $taxonomy ); if ( is_array( $input[ $key ] ) ) { $terms = $input[ $key ]; } else { $terms = explode( ',', $input[ $key ] ); } foreach ( $terms as $term ) { /** * `curl --data 'category[]=123'` should be interpreted as a category ID, * not a category whose name is '123'. * * Consequence: To add a category/tag whose name is '123', the client must * first look up its ID. */ $term = (string) $term; // ctype_digit compat. if ( ctype_digit( $term ) ) { $term = (int) $term; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // A term ID that doesn't already exist. Ignore it: we don't know what name to give it. if ( is_int( $term ) ) { continue; } // only add a new tag/cat if the user has access to. $tax = get_taxonomy( $taxonomy ); // see https://core.trac.wordpress.org/ticket/26409 . if ( 'category' === $taxonomy && ! current_user_can( $tax->cap->edit_terms ) ) { continue; } elseif ( ! current_user_can( $tax->cap->assign_terms ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( ! is_wp_error( $term_info ) ) { if ( $is_hierarchical ) { // Categories must be added by ID. $tax_input[ $taxonomy ][] = (int) $term_info['term_id']; } elseif ( is_int( $term ) ) { // Tags must be added by name. $term = get_term( $term, $taxonomy ); $tax_input[ $taxonomy ][] = $term->name; } else { $tax_input[ $taxonomy ][] = $term; } } } } if ( isset( $input['categories'] ) && empty( $tax_input['category'] ) && 'revision' !== $post_type->name ) { $tax_input['category'][] = get_option( 'default_category' ); } unset( $input['tags'], $input['categories'] ); $insert = array(); if ( ! empty( $input['slug'] ) ) { $insert['post_name'] = $input['slug']; unset( $input['slug'] ); } if ( isset( $input['comments_open'] ) ) { $insert['comment_status'] = ( true === $input['comments_open'] ) ? 'open' : 'closed'; } if ( isset( $input['pings_open'] ) ) { $insert['ping_status'] = ( true === $input['pings_open'] ) ? 'open' : 'closed'; } unset( $input['comments_open'], $input['pings_open'] ); if ( isset( $input['menu_order'] ) ) { $insert['menu_order'] = $input['menu_order']; unset( $input['menu_order'] ); } $publicize = isset( $input['publicize'] ) ? $input['publicize'] : null; unset( $input['publicize'] ); $publicize_custom_message = isset( $input['publicize_message'] ) ? $input['publicize_message'] : null; unset( $input['publicize_message'] ); if ( isset( $input['featured_image'] ) ) { $featured_image = trim( $input['featured_image'] ); $delete_featured_image = empty( $featured_image ); unset( $input['featured_image'] ); } $metadata = isset( $input['metadata'] ) ? $input['metadata'] : null; unset( $input['metadata'] ); $likes = isset( $input['likes_enabled'] ) ? $input['likes_enabled'] : null; unset( $input['likes_enabled'] ); $sharing = isset( $input['sharing_enabled'] ) ? $input['sharing_enabled'] : null; unset( $input['sharing_enabled'] ); $sticky = isset( $input['sticky'] ) ? $input['sticky'] : null; unset( $input['sticky'] ); foreach ( $input as $key => $value ) { $insert[ "post_$key" ] = $value; } if ( ! empty( $author_id ) ) { $insert['post_author'] = absint( $author_id ); } if ( ! empty( $tax_input ) ) { $insert['tax_input'] = $tax_input; } $has_media = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false; $has_media_by_url = isset( $input['media_urls'] ) && $input['media_urls'] ? count( $input['media_urls'] ) : false; if ( $new ) { if ( isset( $input['content'] ) && ! has_shortcode( $input['content'], 'gallery' ) && ( $has_media || $has_media_by_url ) ) { switch ( ( $has_media + $has_media_by_url ) ) { case 0: // No images - do nothing. break; case 1: // 1 image - make it big. $input['content'] = "[gallery size=full columns=1]\n\n" . $input['content']; $insert['post_content'] = $input['content']; break; default: // Several images - 3 column gallery. $input['content'] = "[gallery]\n\n" . $input['content']; $insert['post_content'] = $input['content']; break; } } $post_id = wp_insert_post( add_magic_quotes( $insert ), true ); } else { $insert['ID'] = $post->ID; // wp_update_post ignores date unless edit_date is set // See: https://codex.wordpress.org/Function_Reference/wp_update_post#Scheduling_posts // See: https://core.trac.wordpress.org/browser/tags/3.9.2/src/wp-includes/post.php#L3302 . if ( isset( $input['date_gmt'] ) || isset( $input['date'] ) ) { $insert['edit_date'] = true; } // this two-step process ensures any changes submitted along with status=trash get saved before trashing. if ( isset( $input['status'] ) && 'trash' === $input['status'] ) { // if we insert it with status='trash', it will get double-trashed, so insert it as a draft first. unset( $insert['status'] ); $post_id = wp_update_post( (object) $insert ); // now call wp_trash_post so post_meta gets set and any filters get called. wp_trash_post( $post_id ); } else { $post_id = wp_update_post( (object) $insert ); } } if ( ! $post_id || is_wp_error( $post_id ) ) { return $post_id; } // make sure this post actually exists and is not an error of some kind (ie, trying to load media in the posts endpoint). $post_check = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( is_wp_error( $post_check ) ) { return $post_check; } if ( $has_media ) { $this->api->trap_wp_die( 'upload_error' ); foreach ( $input['media'] as $media_item ) { $_FILES['.api.media.item.'] = $media_item; // check for WP_Error if we ever actually need $media_id . $media_id = media_handle_upload( '.api.media.item.', $post_id ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable } $this->api->trap_wp_die( null ); unset( $_FILES['.api.media.item.'] ); } if ( $has_media_by_url ) { foreach ( $input['media_urls'] as $url ) { $this->handle_media_sideload( $url, $post_id ); } } // Set like status for the post. /** This filter is documented in modules/likes.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); if ( $new ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( isset( $likes ) ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( true === $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } // Set sharing status of the post. if ( $new ) { $sharing_enabled = isset( $sharing ) ? (bool) $sharing : true; if ( false === $sharing_enabled ) { update_post_meta( $post_id, 'sharing_disabled', 1 ); } } elseif ( isset( $sharing ) && true === $sharing ) { delete_post_meta( $post_id, 'sharing_disabled' ); } elseif ( isset( $sharing ) && false == $sharing ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual update_post_meta( $post_id, 'sharing_disabled', 1 ); } if ( isset( $sticky ) ) { if ( true === $sticky ) { stick_post( $post_id ); } else { unstick_post( $post_id ); } } // WPCOM Specific (Jetpack's will get bumped elsewhere // Tracks how many posts are published and sets meta // so we can track some other cool stats (like likes & comments on posts published). if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( ( $new && 'publish' === $input['status'] ) || ( ! $new && isset( $last_status ) && 'publish' !== $last_status && isset( $new_status ) && 'publish' === $new_status ) ) { /** This action is documented in modules/widgets/social-media-icons.php */ do_action( 'jetpack_bump_stats_extras', 'api-insights-posts', $this->api->token_details['client_id'] ); update_post_meta( $post_id, '_rest_api_published', 1 ); update_post_meta( $post_id, '_rest_api_client_id', $this->api->token_details['client_id'] ); } } // We ask the user/dev to pass Publicize services he/she wants activated for the post, but Publicize expects us // to instead flag the ones we don't want to be skipped. proceed with said logic. // any posts coming from Path (client ID 25952) should also not publicize. if ( false === $publicize || ( isset( $this->api->token_details['client_id'] ) && 25952 === (int) $this->api->token_details['client_id'] ) ) { // No publicize at all, skip all by ID. foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); if ( ! $service_connections ) { continue; } foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } } elseif ( is_array( $publicize ) && ( $publicize !== array() ) ) { foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { /* * We support both indexed and associative arrays: * * indexed are to pass entire services * * associative are to pass specific connections per service * * We do support mixed arrays: mixed integer and string keys (see 3rd example below). * * EG: array( 'linkedin', 'facebook') will only publicize to those, ignoring the other available services * Form data: publicize[]=linkedin&publicize[]=facebook * EG: array( 'linkedin' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two LinkedIn accounts, and one Facebook connection, of potentially many. * Form data: publicize[linkedin]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7 * EG: array( 'linkedin', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available LinkedIn accounts, but only 2 of potentially many Facebook connections * Form data: publicize[]=linkedin&publicize[facebook]=$pub_conn_id_0,$pub_conn_id_3 */ // Delete any stale SKIP value for the service by name. We'll add it back by ID. delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); // Get the user's connections. $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); // if the user doesn't have any connections for this service, move on. if ( ! $service_connections ) { continue; } if ( ! in_array( $name, $publicize, true ) && ! array_key_exists( $name, $publicize ) ) { // Skip the whole service by adding each connection ID. foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } elseif ( ! empty( $publicize[ $name ] ) ) { // Seems we're being asked to only push to [a] specific connection[s]. // Explode the list on commas, which will also support a single passed ID. $requested_connections = explode( ',', ( preg_replace( '/[\s]*/', '', $publicize[ $name ] ) ) ); // Flag the connections we can't match with the requested list to be skipped. foreach ( $service_connections as $service_connection ) { if ( ! in_array( $service_connection->meta['connection_data']->id, $requested_connections, true ) ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } else { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } else { // delete all SKIP values; it's okay to publish to all connected IDs for this service. foreach ( $service_connections as $service_connection ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } } if ( $publicize_custom_message !== null ) { if ( empty( $publicize_custom_message ) ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS ); } else { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) ); } } if ( ! empty( $insert['post_format'] ) ) { if ( 'default' !== strtolower( $insert['post_format'] ) ) { set_post_format( $post_id, $insert['post_format'] ); } else { set_post_format( $post_id, get_option( 'default_post_format' ) ); } } if ( isset( $featured_image ) ) { $this->parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ); } if ( ! empty( $metadata ) ) { foreach ( (array) $metadata as $meta ) { $meta = (object) $meta; if ( in_array( $meta->key, Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) && ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 ); } $existing_meta_item = new stdClass(); if ( empty( $meta->operation ) ) { $meta->operation = 'update'; } if ( ! empty( $meta->value ) ) { if ( 'true' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = true; } if ( 'false' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = false; } } if ( ! empty( $meta->id ) ) { $meta->id = absint( $meta->id ); $existing_meta_item = get_metadata_by_mid( 'post', $meta->id ); if ( $post_id !== (int) $existing_meta_item->post_id ) { // Only allow updates for metadata on this post. continue; } } $unslashed_meta_key = wp_unslash( $meta->key ); // should match what the final key will be. $meta->key = wp_slash( $meta->key ); $unslashed_existing_meta_key = wp_unslash( $existing_meta_item->meta_key ); $existing_meta_item->meta_key = wp_slash( $existing_meta_item->meta_key ); // make sure that the meta id passed matches the existing meta key. if ( ! empty( $meta->id ) && ! empty( $meta->key ) ) { $meta_by_id = get_metadata_by_mid( 'post', $meta->id ); if ( $meta_by_id->meta_key !== $meta->key ) { continue; // skip this meta. } } switch ( $meta->operation ) { case 'delete': if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) { delete_metadata_by_mid( 'post', $meta->id ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key ); } break; case 'add': if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) { break; } elseif ( ! empty( $meta->key ) && ! empty( $meta->value ) && ( current_user_can( 'add_post_meta', $post_id, $unslashed_meta_key ) ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) { add_post_meta( $post_id, $meta->key, $meta->value ); } break; case 'update': if ( ! isset( $meta->value ) ) { break; } elseif ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_existing_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_metadata_by_mid( 'post', $meta->id, $meta->value ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value ); } break; } } } /** * Fires when a post is created via the REST API. * * @module json-api * * @since 2.3.0 * * @param int $post_id Post ID. * @param array $insert Data used to build the post. * @param string $new New post URL suffix. */ do_action( 'rest_api_inserted_post', $post_id, $insert, $new ); $return = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } if ( isset( $input['type'] ) && 'revision' === $input['type'] ) { $return['preview_nonce'] = wp_create_nonce( 'post_preview_' . $input['parent'] ); } if ( isset( $sticky ) ) { // workaround for sticky test occasionally failing, maybe a race condition with stick_post() above. $return['sticky'] = ( true === $sticky ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); return $return; } /** * Delete a post. * * /sites/%s/posts/%d/delete -> $blog_id, $post_id * * @param string $path API path. * @param array $blog_id Blog ID. * @param array $post_id Post ID. * * @return array|WP_Error */ public function delete_post( $path, $blog_id, $post_id ) { $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! $this->is_post_type_allowed( $post->post_type ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } if ( ! current_user_can( 'delete_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot delete posts', 403 ); } $args = $this->query_args(); $return = $this->get_post_by( 'ID', $post->ID, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); // we need to call wp_trash_post so that untrash will work correctly for all post types. if ( 'trash' === $post->post_status ) { wp_delete_post( $post->ID ); } else { wp_trash_post( $post->ID ); } $status = get_post_status( $post->ID ); if ( false === $status ) { $return['status'] = 'deleted'; return $return; } return $this->get_post_by( 'ID', $post->ID, $args['context'] ); } /** * Restore a post. * * /sites/%s/posts/%d/restore -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. * * @return array|WP_Error */ public function restore_post( $path, $blog_id, $post_id ) { $args = $this->query_args(); $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! current_user_can( 'delete_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot restore trashed posts', 403 ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); wp_untrash_post( $post->ID ); return $this->get_post_by( 'ID', $post->ID, $args['context'] ); } /** * Set or delete a post's featured image. * * @param int $post_id Post ID. * @param bool $delete_featured_image Whether to delete the featured image. * @param int $featured_image Thumbnail ID to attach. * * @return null|int|bool */ private function parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ) { if ( $delete_featured_image ) { delete_post_thumbnail( $post_id ); return; } $featured_image = (string) $featured_image; // if we got a post ID, we can just set it as the thumbnail. if ( ctype_digit( $featured_image ) && 'attachment' === get_post_type( $featured_image ) ) { set_post_thumbnail( $post_id, $featured_image ); return $featured_image; } $featured_image_id = $this->handle_media_sideload( $featured_image, $post_id, 'image' ); if ( empty( $featured_image_id ) || ! is_int( $featured_image_id ) ) { return false; } set_post_thumbnail( $post_id, $featured_image_id ); return $featured_image_id; } /** * Get the Author ID for a post. * * @param int|string $author Author ID. * @param string $post_type Post type. * * @return int|WP_Error */ private function parse_and_set_author( $author = null, $post_type = 'post' ) { if ( empty( $author ) || ! post_type_supports( $post_type, 'author' ) ) { return get_current_user_id(); } $author = (string) $author; if ( ctype_digit( $author ) ) { $_user = get_user_by( 'id', $author ); if ( ! $_user || is_wp_error( $_user ) ) { return new WP_Error( 'invalid_author', 'Invalid author provided' ); } return $_user->ID; } $_user = get_user_by( 'login', $author ); if ( ! $_user || is_wp_error( $_user ) ) { return new WP_Error( 'invalid_author', 'Invalid author provided' ); } return $_user->ID; } } class.wpcom-json-api-post-v1-1-endpoint.php 0000644 00000032505 14722054026 0014516 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Post v1_1 Endpoint class. */ abstract class WPCOM_JSON_API_Post_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore PEAR.NamingConventions.ValidClassName.Invalid, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * Post object format. * * @var array */ public $post_object_format = array( // explicitly document and cast all output 'ID' => '(int) The post ID.', 'site_ID' => '(int) The site ID.', 'author' => '(object>author) The author of the post.', 'date' => "(ISO 8601 datetime) The post's creation time.", 'modified' => "(ISO 8601 datetime) The post's most recent update time.", 'title' => '(HTML) <code>context</code> dependent.', 'URL' => '(URL) The full permalink URL to the post.', 'short_URL' => '(URL) The wp.me short URL.', 'content' => '(HTML) <code>context</code> dependent.', 'excerpt' => '(HTML) <code>context</code> dependent.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'guid' => '(string) The GUID for the post.', 'status' => array( 'publish' => 'The post is published.', 'draft' => 'The post is saved as a draft.', 'pending' => 'The post is pending editorial approval.', 'private' => 'The post is published privately', 'future' => 'The post is scheduled for future publishing.', 'trash' => 'The post is in the trash.', 'auto-draft' => 'The post is a placeholder for a new post.', ), 'sticky' => '(bool) Is the post sticky?', 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(object>post_reference|false) A reference to the post's parent, if it has one.", 'type' => "(string) The post's post_type. Post types besides post, page and revision need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'discussion' => '(object) Hash of discussion options for the post', 'likes_enabled' => '(bool) Is the post open to likes?', 'sharing_enabled' => '(bool) Should sharing buttons show on this post?', 'like_count' => '(int) The number of likes for this post.', 'i_like' => '(bool) Does the current user like this post?', 'is_reblogged' => '(bool) Did the current user reblog this post?', 'is_following' => '(bool) Is the current user following this blog?', 'global_ID' => '(string) A unique WordPress.com-wide representation of a post.', 'featured_image' => '(URL) The URL to the featured image for this post if it has one.', 'post_thumbnail' => '(object>attachment) The attachment object for the featured image if it has one.', 'format' => array(), // see constructor 'geo' => '(object>geo|false)', 'menu_order' => '(int) (Pages Only) The order pages should appear in.', 'page_template' => '(string) (Pages Only) The page template this page is using.', 'publicize_URLs' => '(array:URL) Array of Facebook URLs published by this post.', 'terms' => '(object) Hash of taxonomy names mapping to a hash of terms keyed by term name.', 'tags' => '(object:tag) Hash of tags (keyed by tag name) applied to the post.', 'categories' => '(object:category) Hash of categories (keyed by category name) applied to the post.', 'attachments' => '(object:attachment) Hash of post attachments (keyed by attachment ID). Returns the most recent 20 attachments. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.', 'attachment_count' => '(int) The total number of attachments for this post. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.', 'metadata' => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'meta' => '(object) API result meta data', 'capabilities' => '(object) List of post-specific permissions for the user; publish_post, edit_post, delete_post', 'revisions' => '(array) List of post revision IDs. Only available for posts retrieved with context=edit.', 'other_URLs' => '(object) List of URLs for this post. Permalink and slug suggestions.', ); /** * Constructor function. * * @param string|array|object $args — Args. */ public function __construct( $args ) { if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) { $this->post_object_format['format'] = get_post_format_strings(); } if ( ! $this->response_format ) { $this->response_format =& $this->post_object_format; } parent::__construct( $args ); } /** * Get a post by a specified field and value * * @param string $field - the field. * @param string $field_value - the field value. * @param string $context Post use context (e.g. 'display'). * * @return array|WP_Error Post **/ public function get_post_by( $field, $field_value, $context = 'display' ) { // validate input if ( ! in_array( $field, array( 'ID', 'name' ), true ) ) { return new WP_Error( 'invalid_field', 'Invalid API FIELD', 400 ); } if ( ! in_array( $context, array( 'display', 'edit' ), true ) ) { return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 ); } if ( 'display' === $context ) { $args = $this->query_args(); if ( isset( $args['content_width'] ) && $args['content_width'] ) { $GLOBALS['content_width'] = (int) $args['content_width']; } } // fetch SAL post $post = $this->get_sal_post_by( $field, $field_value, $context ); if ( is_wp_error( $post ) ) { return $post; } $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // TODO: not sure where this one should go if ( 'display' === $context ) { setup_postdata( $post ); } $keys_to_render = array_keys( $this->post_object_format ); if ( isset( $this->api->query['fields'] ) ) { $limit_to_fields = array_map( 'trim', explode( ',', $this->api->query['fields'] ) ); $keys_to_render = array_intersect( $keys_to_render, $limit_to_fields ); } // always include some keys because processors require it to validate access $keys_to_render = array_unique( array_merge( $keys_to_render, array( 'type', 'status', 'password' ) ) ); $response = $this->render_response_keys( $post, $context, $keys_to_render ); unset( $GLOBALS['post'] ); return $response; } /** * Get SAL post by a specified field and value * * @param string $field - the field. * @param string $field_value - the field value. * @param string $context Post use context (e.g. 'display'). * * @return SAL_Post|WP_Error Post **/ protected function get_sal_post_by( $field, $field_value, $context ) { global $blog_id; $site = $this->get_platform()->get_site( $blog_id ); $post = ( $field === 'name' ) ? $site->get_post_by_name( $field_value, $context ) : $site->get_post_by_id( $field_value, $context ); return $post; } /** * Render the response keys. * * @param object $post - the post. * @param string $context Post use context (e.g. 'display'). * @param array $keys - the keys. * * @return array */ private function render_response_keys( $post, $context, $keys ) { $response = array(); foreach ( $keys as $key ) { switch ( $key ) { case 'ID': // explicitly cast all output $response[ $key ] = (int) $post->ID; break; case 'site_ID': $response[ $key ] = $post->site->get_id(); break; case 'author': $response[ $key ] = $post->get_author(); break; case 'date': $response[ $key ] = $post->get_date(); break; case 'modified': $response[ $key ] = $post->get_modified_date(); break; case 'title': $response[ $key ] = $post->get_title(); break; case 'URL': $response[ $key ] = $post->get_url(); break; case 'short_URL': $response[ $key ] = $post->get_shortlink(); break; case 'content': $response[ $key ] = $post->get_content(); break; case 'excerpt': $response[ $key ] = $post->get_excerpt(); break; case 'status': $response[ $key ] = $post->get_status(); break; case 'sticky': $response[ $key ] = $post->is_sticky(); break; case 'slug': $response[ $key ] = $post->get_slug(); break; case 'guid': $response[ $key ] = $post->get_guid(); break; case 'password': $response[ $key ] = $post->get_password(); break; /** (object|false) */ case 'parent': $response[ $key ] = $post->get_parent(); break; case 'type': $response[ $key ] = $post->get_type(); break; case 'discussion': $response[ $key ] = $post->get_discussion(); break; case 'likes_enabled': $response[ $key ] = $post->is_likes_enabled(); break; case 'sharing_enabled': $response[ $key ] = $post->is_sharing_enabled(); break; case 'like_count': $response[ $key ] = $post->get_like_count(); break; case 'i_like': $response[ $key ] = $post->is_liked(); break; case 'is_reblogged': $response[ $key ] = $post->is_reblogged(); break; case 'is_following': $response[ $key ] = $post->is_following(); break; case 'global_ID': $response[ $key ] = $post->get_global_id(); break; case 'featured_image': $response[ $key ] = $post->get_featured_image(); break; case 'post_thumbnail': $response[ $key ] = $post->get_post_thumbnail(); break; case 'format': $response[ $key ] = $post->get_format(); break; /** (object|false) */ case 'geo': $response[ $key ] = $post->get_geo(); break; case 'menu_order': $response[ $key ] = $post->get_menu_order(); break; case 'page_template': $response[ $key ] = $post->get_page_template(); break; case 'publicize_URLs': $response[ $key ] = $post->get_publicize_urls(); break; case 'terms': $response[ $key ] = $post->get_terms(); break; case 'tags': $response[ $key ] = $post->get_tags(); break; case 'categories': $response[ $key ] = $post->get_categories(); break; case 'attachments': list( $attachments, $attachment_count ) = $post->get_attachments_and_count(); $response[ $key ] = $attachments; $response['attachment_count'] = $attachment_count; break; /** (array|false) */ case 'metadata': $response[ $key ] = $post->get_metadata(); break; case 'meta': $response[ $key ] = $post->get_meta(); break; case 'capabilities': $response[ $key ] = $post->get_current_user_capabilities(); break; case 'revisions': $revisions = $post->get_revisions(); if ( $revisions ) { $response[ $key ] = $revisions; } break; case 'other_URLs': $response[ $key ] = $post->get_other_urls(); break; } } return $response; } /** * Filter respnse. * * @param array $response - the response. * @return array Filtered response. */ public function filter_response( $response ) { // Do minimal processing if the caller didn't request it if ( ! isset( $_REQUEST['meta_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site. return $response; } // Bail early if we do not have the necessary data. if ( is_wp_error( $response ) || ! isset( $response['posts'] ) || ! is_array( $response['posts'] ) ) { return $response; } // Retrieve an array of field paths, such as: [`autosave.modified`, `autosave.post_ID`] $fields = explode( ',', sanitize_text_field( wp_unslash( $_REQUEST['meta_fields'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site. foreach ( $response['posts'] as $post ) { if ( ! isset( $post['meta'] ) || ! isset( $post['meta']->data ) || ( ! is_array( $post['meta']->data ) && ! is_object( $post['meta']->data ) ) ) { continue; } $newmeta = array(); foreach ( $post['meta']->data as $field_key => $field_value ) { foreach ( $field_value as $subfield_key => $subfield_value ) { $key_path = $field_key . '.' . $subfield_key; if ( in_array( $key_path, $fields, true ) ) { $newmeta[ $field_key ][ $subfield_key ] = $subfield_value; } } } $post['meta']->data = $newmeta; } return $response; } /** * Gets the blog post. * * @param int $blog_id - the blog ID. * @param int $post_id - the post ID. * @param string $context - the context. * * @return array|bool|WP_Error */ public function get_blog_post( $blog_id, $post_id, $context = 'display' ) { $blog_id = $this->api->get_blog_id( $blog_id ); if ( ! $blog_id || is_wp_error( $blog_id ) ) { return $blog_id; } switch_to_blog( $blog_id ); $post = $this->get_post_by( 'ID', $post_id, $context ); restore_current_blog(); return $post; } } class.wpcom-json-api-update-taxonomy-endpoint.php 0000644 00000027436 14722054026 0016214 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update site taxonomy API endpoints. * * Endpoints: * Create a new category: /sites/%s/categories/new * Create a new tag: /sites/%s/tags/new * Edit a category: /sites/%s/categories/slug:%s * Edit a tag: /sites/%s/tags/slug:%s * Delete a category: /sites/%s/categories/slug:%s/delete * Delete a tag: /sites/%s/tags/slug:%s/delete */ new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Create a new category.', 'group' => 'taxonomy', 'stat' => 'categories:new', 'method' => 'POST', 'path' => '/sites/%s/categories/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'name' => '(string) Name of the category', 'description' => '(string) A description of the category', 'parent' => '(int) ID of the parent category', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'name' => 'Puppies', ), ), ) ); new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Create a new tag.', 'group' => 'taxonomy', 'stat' => 'tags:new', 'method' => 'POST', 'path' => '/sites/%s/tags/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'name' => '(string) Name of the tag', 'description' => '(string) A description of the tag', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'name' => 'Kitties', ), ), ) ); new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Edit a tag.', 'group' => 'taxonomy', 'stat' => 'tags:1:POST', 'method' => 'POST', 'path' => '/sites/%s/tags/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$tag' => '(string) The tag slug', ), 'request_format' => array( 'name' => '(string) Name of the tag', 'description' => '(string) A description of the tag', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/slug:testing-tag', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'description' => 'Kitties are awesome!', ), ), ) ); new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Edit a category.', 'group' => 'taxonomy', 'stat' => 'categories:1:POST', 'method' => 'POST', 'path' => '/sites/%s/categories/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$category' => '(string) The category slug', ), 'request_format' => array( 'name' => '(string) Name of the category', 'description' => '(string) A description of the category', 'parent' => '(int) ID of the parent category', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/slug:testing-category', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'description' => 'Puppies are great!', ), ), ) ); new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Delete a category.', 'group' => 'taxonomy', 'stat' => 'categories:1:delete', 'method' => 'POST', 'path' => '/sites/%s/categories/slug:%s/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$category' => '(string) The category slug', ), 'response_format' => array( 'slug' => '(string) The slug of the deleted category', 'success' => '(bool) Was the operation successful?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/slug:$category/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array( 'description' => 'Delete a tag.', 'group' => 'taxonomy', 'stat' => 'tags:1:delete', 'method' => 'POST', 'path' => '/sites/%s/tags/slug:%s/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$tag' => '(string) The tag slug', ), 'response_format' => array( 'slug' => '(string) The slug of the deleted tag', 'success' => '(bool) Was the operation successful?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/slug:$tag/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Update site taxonomy API class. */ class WPCOM_JSON_API_Update_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint { /** * Update site taxonomy API callback. * * - /sites/%s/tags|categories/new -> $blog_id * - /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id * - /sites/%s/tags|categories/slug:%s/delete -> $blog_id, $taxonomy_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int|string $object_id Term. */ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( preg_match( '#/tags/#i', $path ) ) { $taxonomy_type = 'post_tag'; } else { $taxonomy_type = 'category'; } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ); } elseif ( $this->api->ends_with( $path, '/new' ) ) { return $this->new_taxonomy( $path, $blog_id, $taxonomy_type ); } return $this->update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ); } /** * Create a new taxonomy. * * - /sites/%s/tags|categories/new -> $blog_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param string $taxonomy_type Taxonomy type (category, post_tag). */ public function new_taxonomy( $path, $blog_id, $taxonomy_type ) { $args = $this->query_args(); $input = $this->input(); if ( ! is_array( $input ) || ! $input || ! strlen( $input['name'] ) ) { return new WP_Error( 'invalid_input', 'Unknown data passed', 400 ); } $user = wp_get_current_user(); if ( ! $user || is_wp_error( $user ) || ! $user->ID ) { return new WP_Error( 'authorization_required', 'An active access token must be used to manage taxonomies.', 403 ); } $tax = get_taxonomy( $taxonomy_type ); if ( ! current_user_can( $tax->cap->edit_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } if ( 'category' !== $taxonomy_type || ! isset( $input['parent'] ) ) { $input['parent'] = 0; } $term = get_term_by( 'name', $input['name'], $taxonomy_type ); if ( $term ) { // the same name is allowed as long as the parents are different. if ( $input['parent'] === $term->parent ) { return new WP_Error( 'duplicate', 'A taxonomy with that name already exists', 400 ); } } $data = wp_insert_term( addslashes( $input['name'] ), $taxonomy_type, array( 'description' => isset( $input['description'] ) ? addslashes( $input['description'] ) : '', 'parent' => $input['parent'], ) ); if ( is_wp_error( $data ) ) { return $data; } $taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type ); $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'taxonomies' ); return $return; } /** * Update a taxonomy. * * - /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int|string $object_id Term. * @param string $taxonomy_type Taxonomy type (category, post_tag). */ public function update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) { $taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type ); $tax = get_taxonomy( $taxonomy_type ); if ( ! current_user_can( $tax->cap->edit_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } if ( ! $taxonomy || is_wp_error( $taxonomy ) ) { return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); } if ( false === term_exists( $object_id, $taxonomy_type ) ) { return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 ); } $args = $this->query_args(); $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $update = array(); if ( 'category' === $taxonomy_type && ! empty( $input['parent'] ) ) { $update['parent'] = $input['parent']; } if ( ! empty( $input['description'] ) ) { $update['description'] = addslashes( $input['description'] ); } if ( ! empty( $input['name'] ) ) { $update['name'] = addslashes( $input['name'] ); } $data = wp_update_term( $taxonomy->term_id, $taxonomy_type, $update ); $taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type ); $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'taxonomies' ); return $return; } /** * Delete a taxonomy. * * - /sites/%s/tags|categories/%s/delete -> $blog_id, $taxonomy_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int|string $object_id Term. * @param string $taxonomy_type Taxonomy type (category, post_tag). */ public function delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) { $taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type ); $tax = get_taxonomy( $taxonomy_type ); if ( ! current_user_can( $tax->cap->delete_terms ) ) { return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); } if ( ! $taxonomy || is_wp_error( $taxonomy ) ) { return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); } if ( false === term_exists( $object_id, $taxonomy_type ) ) { return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 ); } $args = $this->query_args(); $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'taxonomies' ); wp_delete_term( $taxonomy->term_id, $taxonomy_type ); return array( 'slug' => (string) $taxonomy->slug, 'success' => 'true', ); } } class.wpcom-json-api-get-media-v1-1-endpoint.php 0000644 00000007176 14722054026 0015373 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Media_v1_1_Endpoint( array( 'description' => 'Get a single media item (by ID).', 'group' => 'media', 'stat' => 'media:1', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'GET', 'path' => '/sites/%s/media/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'response_format' => array( 'ID' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'post_ID' => '(int) ID of the post this media is attached to', 'author_ID' => '(int) ID of the user who uploaded the media', 'URL' => '(string) URL to the file', 'guid' => '(string) Unique identifier', 'file' => '(string) Filename', 'extension' => '(string) File extension', 'mime_type' => '(string) File MIME type', 'title' => '(string) Filename', 'caption' => '(string) User-provided caption of the file', 'description' => '(string) Description of the file', 'alt' => '(string) Alternative text for image files.', 'thumbnails' => '(object) Media item thumbnail URL options', 'height' => '(int) (Image & video only) Height of the media item', 'width' => '(int) (Image & video only) Width of the media item', 'length' => '(int) (Video & audio only) Duration of the media item, in seconds', 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item', 'rating' => '(string) (Video only) VideoPress rating of the video', 'display_embed' => '(string) Video only. Whether to share or not the video.', 'allow_download' => '(string) Video only. Whether the video can be downloaded or not.', 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress', 'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/934', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * GET Media v1_1 endpoint. */ class WPCOM_JSON_API_Get_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { //phpcs:ignore /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } // upload_files can probably be used for other endpoints but we want contributors to be able to use media too. if ( ! current_user_can( 'edit_posts', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } return $this->get_media_item_v1_1( $media_id ); } } class.wpcom-json-api-update-post-v1-2-endpoint.php 0000644 00000133106 14722054026 0015776 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update post endpoint v1.2 * * Endpoints: * Create a post: /sites/%s/posts/new * Update a post: /sites/%s/posts/%d * Delete a post: /sites/%s/posts/%d/delete * Restore a post: /sites/%s/posts/%d/restore */ new WPCOM_JSON_API_Update_Post_v1_2_Endpoint( array( 'description' => 'Create a post.', 'group' => 'posts', 'stat' => 'posts:new', 'min_version' => '1.2', 'max_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/posts/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'autosave' => '(bool) True if the post was saved automatically.', ), 'request_format' => array( // explicitly document all input. 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'pending' => 'Mark the post as pending editorial approval.', 'future' => 'Schedule the post (alias for publish; you must also set a future date).', 'auto-draft' => 'Save a placeholder for a newly created post, with no content.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'type' => "(string) The post type. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'terms' => '(object) Mapping of taxonomy to comma-separated list or array of term names', 'categories' => '(array|string) Comma-separated list or array of category names', 'tags' => '(array|string) Comma-separated list or array of tag names', 'terms_by_id' => '(object) Mapping of taxonomy to comma-separated list or array of term IDs', 'categories_by_id' => '(array|string) Comma-separated list or array of category IDs', 'tags_by_id' => '(array|string) Comma-separated list or array of tag IDs', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint. Errors produced by media uploads, if any, will be in `media_errors` in the response. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image Post' \<br />--form 'media[0]=@/path/to/file.jpg' \<br />--form 'media_attrs[0][caption]=My Great Photo' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post. Errors produced by media sideloading, if any, will be in `media_errors` in the response.', 'media_attrs' => '(array) An array of attributes (`title`, `description` and `caption`) are supported to assign to the media uploaded via the `media` or `media_urls` properties. You must use a numeric index for the keys of `media_attrs` which follow the same sequence as `media` and `media_urls`. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Gallery Post' \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://exapmple.com/file2.jpg' \<br /> \<br />--form 'media_attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'media_attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'discussion' => '(object) A hash containing one or more of the following boolean values, which default to the blog\'s discussion preferences: `comments_open`, `pings_open`', 'likes_enabled' => "(bool) Should the post be open to likes? Defaults to the blog's preference.", 'sharing_enabled' => '(bool) Should sharing buttons show on this post? Defaults to true.', 'menu_order' => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.', 'page_template' => '(string) (Pages Only) The page template this page should use.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/posts/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World', 'content' => 'Hello. I am a test post. I was created by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); new WPCOM_JSON_API_Update_Post_v1_2_Endpoint( array( 'description' => 'Edit a post.', 'group' => 'posts', 'stat' => 'posts:1:POST', 'min_version' => '1.2', 'max_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/posts/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'query_parameters' => array( 'autosave' => '(bool) True if the post was saved automatically.', ), 'request_format' => array( 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'future' => 'Schedule the post (alias for publish; you must also set a future date).', 'pending' => 'Mark the post as pending editorial approval.', 'trash' => 'Set the post as trashed.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'terms' => '(object) Mapping of taxonomy to comma-separated list or array of term names', 'terms_by_id' => '(object) Mapping of taxonomy to comma-separated list or array of term IDs', 'categories' => '(array|string) Comma-separated list or array of category names', 'categories_by_id' => '(array|string) Comma-separated list or array of category IDs', 'tags' => '(array|string) Comma-separated list or array of tag names', 'tags_by_id' => '(array|string) Comma-separated list or array of tag IDs', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'discussion' => '(object) A hash containing one or more of the following boolean values, which default to the blog\'s discussion preferences: `comments_open`, `pings_open`', 'likes_enabled' => '(bool) Should the post be open to likes?', 'menu_order' => '(int) (Pages only) the order pages should appear in. Use 0 to maintain alphabetical order.', 'page_template' => '(string) (Pages Only) The page template this page should use.', 'sharing_enabled' => '(bool) Should sharing buttons show on this post?', 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options resposne of the site endpoint. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.', 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'if_not_modified_since' => '(ISO 8601 datetime) If the post has been modified since this time, the post will not be updated.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/posts/881', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World (Again)', 'content' => 'Hello. I am an edited post. I was edited by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); use function Automattic\Jetpack\Extensions\Map\map_block_from_geo_points; // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Update post v1.2 endpoint class. */ class WPCOM_JSON_API_Update_Post_v1_2_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint { /** * Create or update a post. * * /sites/%s/posts/new -> $blog_id * /sites/%s/posts/%d -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. */ public function write_post( $path, $blog_id, $post_id ) { $delete_featured_image = null; $media_results = array(); global $wpdb; $new = $this->api->ends_with( $path, '/new' ); $args = $this->query_args(); if ( ! empty( $args['autosave'] ) ) { define( 'DOING_AUTOSAVE', true ); } // unhook publicize, it's hooked again later -- without this, skipping services is impossible. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { remove_action( 'save_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ), 100, 2 ); if ( $this->should_load_theme_functions( $post_id ) ) { $this->load_theme_functions(); } } if ( $new ) { $input = $this->input( true ); // 'future' is an alias for 'publish' for now if ( isset( $input['status'] ) && 'future' === $input['status'] ) { $input['status'] = 'publish'; } // default to post. if ( empty( $input['type'] ) ) { $input['type'] = 'post'; } if ( 'revision' === $input['type'] ) { if ( ! isset( $input['parent'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $input['status'] = 'inherit'; // force inherit for revision type. $input['slug'] = $input['parent'] . '-autosave-v1'; } elseif ( ! isset( $input['title'] ) && ! isset( $input['content'] ) && ! isset( $input['excerpt'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $post_type = get_post_type_object( $input['type'] ); if ( ! $this->is_post_type_allowed( $input['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } if ( ! empty( $input['author'] ) ) { $author_id = parent::parse_and_set_author( $input['author'], $input['type'] ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( 'publish' === $input['status'] ) { if ( ! current_user_can( $post_type->cap->publish_posts ) ) { if ( current_user_can( $post_type->cap->edit_posts ) ) { $input['status'] = 'pending'; } else { return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 ); } } } elseif ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 ); } } else { $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( isset( $input['status'] ) && 'trash' === $input['status'] && ! current_user_can( 'delete_post', $post_id ) ) { return new WP_Error( 'unauthorized', 'User cannot delete post', 403 ); } // 'future' is an alias for 'publish' for now if ( isset( $input['status'] ) && 'future' === $input['status'] ) { $input['status'] = 'publish'; } $_post_type = ( ! empty( $input['type'] ) ) ? $input['type'] : $post->post_type; $post_type = get_post_type_object( $_post_type ); if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } // The input `if_not_modified_since` input is the format ISO 8601 datetime and get converted to `if_not_modified_since_gmt` and `if_not_modified_since` if ( ! empty( $input['if_not_modified_since_gmt'] ) ) { if ( mysql2date( 'U', $post->post_modified_gmt ) > mysql2date( 'U', $input['if_not_modified_since_gmt'] ) ) { return new WP_Error( 'old-revision', 'There is a revision of this post that is more recent.', 409 ); } } if ( ! empty( $input['author'] ) ) { $author_id = parent::parse_and_set_author( $input['author'], $_post_type ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( ( isset( $input['status'] ) && 'publish' === $input['status'] ) && 'publish' !== $post->post_status && ! current_user_can( 'publish_post', $post->ID ) ) { $input['status'] = 'pending'; } $last_status = $post->post_status; $new_status = isset( $input['status'] ) ? $input['status'] : $last_status; // Make sure that drafts get the current date when transitioning to publish if not supplied in the post. // Similarly, scheduled posts that are manually published before their scheduled date should have the date reset. $date_in_past = ( strtotime( $post->post_date_gmt ) < time() ); $reset_draft_date = 'publish' === $new_status && 'draft' === $last_status && ! isset( $input['date_gmt'] ) && $date_in_past; $reset_scheduled_date = 'publish' === $new_status && 'future' === $last_status && ! isset( $input['date_gmt'] ) && ! $date_in_past; if ( $reset_draft_date || $reset_scheduled_date ) { $input['date_gmt'] = gmdate( 'Y-m-d H:i:s' ); } // Untrash a post so that the proper hooks get called as well as the comments get untrashed. if ( $this->should_untrash_post( $last_status, $new_status, $post ) ) { $input = $this->untrash_post( $post, $input ); } } if ( function_exists( 'wpcom_switch_to_blog_locale' ) ) { // fixes calypso-pre-oss #12476: respect blog locale when creating the post slug. wpcom_switch_to_blog_locale( $blog_id ); } // If date is set, $this->input will set date_gmt, date still needs to be adjusted. if ( isset( $input['date_gmt'] ) ) { $gmt_offset = get_option( 'gmt_offset' ); $time_with_offset = strtotime( $input['date_gmt'] ) + $gmt_offset * HOUR_IN_SECONDS; $input['date'] = gmdate( 'Y-m-d H:i:s', $time_with_offset ); } if ( ! empty( $author_id ) && get_current_user_id() !== $author_id ) { if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'unauthorized', "User is not allowed to publish others' posts.", 403 ); } elseif ( ! user_can( $author_id, $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'Assigned author cannot publish post.', 403 ); } } if ( ! is_post_type_hierarchical( $post_type->name ) && 'revision' !== $post_type->name ) { unset( $input['parent'] ); } foreach ( array( '', '_by_id' ) as $term_key_suffix ) { $term_input_key = 'terms' . $term_key_suffix; if ( isset( $input[ $term_input_key ] ) ) { $input[ $term_input_key ] = (array) $input[ $term_input_key ]; } else { $input[ $term_input_key ] = array(); } // Convert comma-separated terms to array before attempting to // merge with hardcoded taxonomies. foreach ( $input[ $term_input_key ] as $taxonomy => $terms ) { if ( is_string( $terms ) ) { $input[ $term_input_key ][ $taxonomy ] = explode( ',', $terms ); } elseif ( ! is_array( $terms ) ) { $input[ $term_input_key ][ $taxonomy ] = array(); } } // For each hard-coded taxonomy, merge into terms object. foreach ( array( 'categories' => 'category', 'tags' => 'post_tag', ) as $key_prefix => $taxonomy ) { $taxonomy_key = $key_prefix . $term_key_suffix; if ( ! isset( $input[ $taxonomy_key ] ) ) { continue; } if ( ! isset( $input[ $term_input_key ][ $taxonomy ] ) ) { $input[ $term_input_key ][ $taxonomy ] = array(); } $terms = $input[ $taxonomy_key ]; if ( is_string( $terms ) ) { $terms = explode( ',', $terms ); } elseif ( ! is_array( $terms ) ) { continue; } $input[ $term_input_key ][ $taxonomy ] = array_merge( $input[ $term_input_key ][ $taxonomy ], $terms ); } } /* add terms by name */ $tax_input = array(); foreach ( $input['terms'] as $taxonomy => $terms ) { $tax_input[ $taxonomy ] = array(); $is_hierarchical = is_taxonomy_hierarchical( $taxonomy ); foreach ( $terms as $term ) { /** * We assume these are names, not IDs, even if they are numeric. * Note: A category named "0" will not work right. * https://core.trac.wordpress.org/ticket/9059 */ if ( ! is_string( $term ) ) { continue; } $term_info = get_term_by( 'name', $term, $taxonomy, ARRAY_A ); if ( ! $term_info ) { // only add a new tag/cat if the user has access to. $tax = get_taxonomy( $taxonomy ); // see https://core.trac.wordpress.org/ticket/26409 . if ( $is_hierarchical && ! current_user_can( $tax->cap->edit_terms ) ) { continue; } elseif ( ! current_user_can( $tax->cap->assign_terms ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( ! is_wp_error( $term_info ) ) { if ( $is_hierarchical ) { // Hierarchical terms must be added by ID. $tax_input[ $taxonomy ][] = (int) $term_info['term_id']; } else { // Non-hierarchical terms must be added by name. $tax_input[ $taxonomy ][] = $term; } } } } /* add terms by ID */ foreach ( $input['terms_by_id'] as $taxonomy => $terms ) { // combine with any previous selections. if ( ! isset( $tax_input[ $taxonomy ] ) || ! is_array( $tax_input[ $taxonomy ] ) ) { $tax_input[ $taxonomy ] = array(); } $is_hierarchical = is_taxonomy_hierarchical( $taxonomy ); foreach ( $terms as $term ) { $term = (string) $term; // ctype_digit compat. if ( ! ctype_digit( $term ) ) { // skip anything that doesn't look like an ID. continue; } $term = (int) $term; $term_info = get_term_by( 'id', $term, $taxonomy, ARRAY_A ); if ( $term_info && ! is_wp_error( $term_info ) ) { if ( $is_hierarchical ) { // Categories must be added by ID. $tax_input[ $taxonomy ][] = $term; } else { // Tags must be added by name. $tax_input[ $taxonomy ][] = $term_info['name']; } } } } if ( ( isset( $input['terms']['category'] ) || isset( $input['terms_by_id']['category'] ) ) && empty( $tax_input['category'] ) && 'revision' !== $post_type->name ) { $tax_input['category'][] = get_option( 'default_category' ); } unset( $input['terms'], $input['tags'], $input['categories'], $input['terms_by_id'], $input['tags_by_id'], $input['categories_by_id'] ); $insert = array(); if ( ! empty( $input['slug'] ) ) { $insert['post_name'] = $input['slug']; unset( $input['slug'] ); } if ( isset( $input['discussion'] ) ) { $discussion = (array) $input['discussion']; foreach ( array( 'comment', 'ping' ) as $discussion_type ) { $discussion_open = sprintf( '%ss_open', $discussion_type ); $discussion_status = sprintf( '%s_status', $discussion_type ); if ( isset( $discussion[ $discussion_open ] ) ) { $is_open = WPCOM_JSON_API::is_truthy( $discussion[ $discussion_open ] ); $discussion[ $discussion_status ] = $is_open ? 'open' : 'closed'; } if ( in_array( $discussion[ $discussion_status ], array( 'open', 'closed' ), true ) ) { $insert[ $discussion_status ] = $discussion[ $discussion_status ]; } } } unset( $input['discussion'] ); if ( isset( $input['menu_order'] ) ) { $insert['menu_order'] = $input['menu_order']; unset( $input['menu_order'] ); } $publicize = isset( $input['publicize'] ) ? $input['publicize'] : null; unset( $input['publicize'] ); $publicize_custom_message = isset( $input['publicize_message'] ) ? $input['publicize_message'] : null; unset( $input['publicize_message'] ); if ( isset( $input['featured_image'] ) ) { $featured_image = trim( $input['featured_image'] ); $delete_featured_image = empty( $featured_image ); unset( $input['featured_image'] ); } $metadata = isset( $input['metadata'] ) ? $input['metadata'] : null; unset( $input['metadata'] ); $likes = isset( $input['likes_enabled'] ) ? $input['likes_enabled'] : null; unset( $input['likes_enabled'] ); $sharing = isset( $input['sharing_enabled'] ) ? $input['sharing_enabled'] : null; unset( $input['sharing_enabled'] ); $sticky = isset( $input['sticky'] ) ? $input['sticky'] : null; unset( $input['sticky'] ); foreach ( $input as $key => $value ) { $insert[ "post_$key" ] = $value; } if ( ! empty( $author_id ) ) { $insert['post_author'] = absint( $author_id ); } if ( ! empty( $tax_input ) ) { $insert['tax_input'] = $tax_input; } $has_media = ! empty( $input['media'] ) ? count( $input['media'] ) : false; $has_media_by_url = ! empty( $input['media_urls'] ) ? count( $input['media_urls'] ) : false; $media_files = array(); $media_urls = array(); $media_attrs = array(); $media_id_string = ''; if ( $has_media || $has_media_by_url ) { $media_files = ! empty( $input['media'] ) ? $input['media'] : array(); $media_urls = ! empty( $input['media_urls'] ) ? $input['media_urls'] : array(); $media_attrs = ! empty( $input['media_attrs'] ) ? $input['media_attrs'] : array(); $media_results = $this->handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs ); $media_id_string = implode( ',', array_filter( array_map( 'absint', $media_results['media_ids'] ) ) ); } $is_dtp_fb_post = false; if ( in_array( '_dtp_fb', wp_list_pluck( (array) $metadata, 'key' ), true ) ) { $is_dtp_fb_post = true; add_filter( 'rest_api_allowed_public_metadata', array( $this, 'dtp_fb_allowed_metadata' ) ); } /** * Log Media details for a Post creation request. * Temporary logging for media data. * * @see p1709028174665519-slack-CDLH4C1UZ * * @since 13.2 * * @param bool $is_dtp_fb_post Is this for a Facebook import? * @param int $blog_id Blog ID. * @param array $input Whole input. * @param array $media_files File upload data. * @param array $media_urls URLs to fetch. * @param array $media_attrs Attributes corresponding to each entry in `$media_files`/`$media_urls`. * @param array $media_results * - media_ids: IDs created, by index in `$media_files`/`$media_urls`. * - errors: Errors encountered, by index in `$media_files`/`$media_urls`. */ do_action( 'jetpack_dtp_fb_media', $is_dtp_fb_post, $blog_id, $input, $media_files, $media_urls, $media_attrs, $media_results ); if ( $new ) { if ( isset( $input['content'] ) && ! has_shortcode( $input['content'], 'gallery' ) && ( $has_media || $has_media_by_url ) ) { switch ( ( $has_media + $has_media_by_url ) ) { case 0: // No images - do nothing. break; case 1: // 1 image - make it big. $input['content'] = sprintf( "[gallery size=full ids='%s' columns=1]\n\n", $media_id_string ) . $input['content']; $insert['post_content'] = $input['content']; break; default: // Several images - 3 column gallery. $input['content'] = sprintf( "[gallery ids='%s']\n\n", $media_id_string ) . $input['content']; $insert['post_content'] = $input['content']; break; } } $insert['post_date'] = isset( $insert['post_date'] ) ? $insert['post_date'] : ''; if ( $is_dtp_fb_post ) { $insert = $this->dtp_fb_preprocess_post( $insert, $metadata ); } $post_id = $this->post_exists( $insert['post_title'], $insert['post_content'], $insert['post_date'], $post_type->name ); if ( 0 === $post_id ) { $post_id = wp_insert_post( add_magic_quotes( $insert ), true ); } } else { $insert['ID'] = $post->ID; // wp_update_post ignores date unless edit_date is set // See: https://codex.wordpress.org/Function_Reference/wp_update_post#Scheduling_posts // See: https://core.trac.wordpress.org/browser/tags/3.9.2/src/wp-includes/post.php#L3302 . if ( isset( $input['date_gmt'] ) || isset( $input['date'] ) ) { $insert['edit_date'] = true; } // this two-step process ensures any changes submitted along with status=trash get saved before trashing. if ( isset( $input['status'] ) && 'trash' === $input['status'] ) { // if we insert it with status='trash', it will get double-trashed, so insert it as a draft first. unset( $insert['status'] ); $post_id = wp_update_post( (object) $insert ); // now call wp_trash_post so post_meta gets set and any filters get called. wp_trash_post( $post_id ); } else { $post_id = wp_update_post( (object) $insert ); } } if ( ! $post_id || is_wp_error( $post_id ) ) { return $post_id; } // make sure this post actually exists and is not an error of some kind (ie, trying to load media in the posts endpoint). $post_check = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( is_wp_error( $post_check ) ) { return $post_check; } if ( $media_id_string ) { // Yes - this is really how wp-admin does it. $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_parent = %d WHERE post_type = 'attachment' AND ID IN ( $media_id_string )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IDs are filtered to absint above. $post_id ) ); foreach ( $media_results['media_ids'] as $media_id ) { clean_attachment_cache( $media_id ); } clean_post_cache( $post_id ); } // set page template for this post. if ( isset( $input['page_template'] ) && 'page' === $post_type->name ) { $page_template = $input['page_template']; $page_templates = wp_get_theme()->get_page_templates( get_post( $post_id ) ); if ( empty( $page_template ) || 'default' === $page_template || isset( $page_templates[ $page_template ] ) ) { update_post_meta( $post_id, '_wp_page_template', $page_template ); } } // Set like status for the post. /** This filter is documented in modules/likes.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); if ( $new ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( isset( $likes ) ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( true === $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } // Set sharing status of the post. if ( $new ) { $sharing_enabled = isset( $sharing ) ? (bool) $sharing : true; if ( false === $sharing_enabled ) { update_post_meta( $post_id, 'sharing_disabled', 1 ); } } elseif ( isset( $sharing ) && true === $sharing ) { delete_post_meta( $post_id, 'sharing_disabled' ); } elseif ( isset( $sharing ) && false == $sharing ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual update_post_meta( $post_id, 'sharing_disabled', 1 ); } if ( isset( $sticky ) ) { if ( true === $sticky ) { stick_post( $post_id ); } else { unstick_post( $post_id ); } } // WPCOM Specific (Jetpack's will get bumped elsewhere // Tracks how many posts are published and sets meta // so we can track some other cool stats (like likes & comments on posts published). if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( ( $new && 'publish' === $input['status'] ) || ( ! $new && isset( $last_status ) && 'publish' !== $last_status && isset( $new_status ) && 'publish' === $new_status ) ) { /** This action is documented in modules/widgets/social-media-icons.php */ do_action( 'jetpack_bump_stats_extras', 'api-insights-posts', $this->api->token_details['client_id'] ); update_post_meta( $post_id, '_rest_api_published', 1 ); update_post_meta( $post_id, '_rest_api_client_id', $this->api->token_details['client_id'] ); } } // We ask the user/dev to pass Publicize services he/she wants activated for the post, but Publicize expects us // to instead flag the ones we don't want to be skipped. proceed with said logic. // any posts coming from Path (client ID 25952) should also not publicize. if ( false === $publicize || ( isset( $this->api->token_details['client_id'] ) && 25952 === (int) $this->api->token_details['client_id'] ) ) { // No publicize at all, skip all by ID. foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); if ( ! $service_connections ) { continue; } foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } } elseif ( is_array( $publicize ) && ( count( $publicize ) > 0 ) ) { foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { /* * We support both indexed and associative arrays: * * indexed are to pass entire services * * associative are to pass specific connections per service * * We do support mixed arrays: mixed integer and string keys (see 3rd example below). * * EG: array( 'linkedin', 'facebook') will only publicize to those, ignoring the other available services * Form data: publicize[]=linkedin&publicize[]=facebook * EG: array( 'linkedin' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two LinkedIn accounts, and one Facebook connection, of potentially many. * Form data: publicize[linkedin]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7 * EG: array( 'linkedin', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available LinkedIn accounts, but only 2 of potentially many Facebook connections * Form data: publicize[]=linkedin&publicize[facebook]=$pub_conn_id_0,$pub_conn_id_3 */ // Delete any stale SKIP value for the service by name. We'll add it back by ID. delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); // Get the user's connections. $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); // if the user doesn't have any connections for this service, move on. if ( ! $service_connections ) { continue; } if ( ! in_array( $name, $publicize, true ) && ! array_key_exists( $name, $publicize ) ) { // Skip the whole service by adding each connection ID. foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } elseif ( ! empty( $publicize[ $name ] ) ) { // Seems we're being asked to only push to [a] specific connection[s]. // Explode the list on commas, which will also support a single passed ID. $requested_connections = explode( ',', ( preg_replace( '/[\s]*/', '', $publicize[ $name ] ) ) ); // Flag the connections we can't match with the requested list to be skipped. foreach ( $service_connections as $service_connection ) { if ( ! in_array( $service_connection->meta['connection_data']->id, $requested_connections, true ) ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } else { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } else { // delete all SKIP values; it's okay to publish to all connected IDs for this service. foreach ( $service_connections as $service_connection ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } } if ( $publicize_custom_message !== null ) { if ( empty( $publicize_custom_message ) ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS ); } else { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) ); } } if ( ! empty( $insert['post_format'] ) ) { if ( 'default' !== strtolower( $insert['post_format'] ) ) { set_post_format( $post_id, $insert['post_format'] ); } else { set_post_format( $post_id, get_option( 'default_post_format' ) ); } } if ( isset( $featured_image ) ) { parent::parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ); } if ( ! empty( $metadata ) ) { foreach ( (array) $metadata as $meta ) { $meta = (object) $meta; if ( in_array( $meta->key, Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) && ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 ); } $existing_meta_item = new stdClass(); if ( empty( $meta->operation ) ) { $meta->operation = 'update'; } if ( ! empty( $meta->value ) ) { if ( 'true' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = true; } if ( 'false' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = false; } } if ( ! empty( $meta->id ) ) { $meta->id = absint( $meta->id ); $existing_meta_item = get_metadata_by_mid( 'post', $meta->id ); if ( $post_id !== (int) $existing_meta_item->post_id ) { // Only allow updates for metadata on this post. continue; } } $unslashed_meta_key = wp_unslash( $meta->key ); // should match what the final key will be. $meta->key = wp_slash( $meta->key ); $unslashed_existing_meta_key = isset( $existing_meta_item->meta_key ) ? wp_unslash( $existing_meta_item->meta_key ) : ''; $existing_meta_item->meta_key = isset( $existing_meta_item->meta_key ) ? wp_slash( $existing_meta_item->meta_key ) : ''; // make sure that the meta id passed matches the existing meta key. if ( ! empty( $meta->id ) && ! empty( $meta->key ) ) { $meta_by_id = get_metadata_by_mid( 'post', $meta->id ); if ( $meta_by_id->meta_key !== $meta->key ) { continue; // skip this meta. } } switch ( $meta->operation ) { case 'delete': if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) { delete_metadata_by_mid( 'post', $meta->id ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key ); } break; case 'add': if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) { break; } elseif ( ! empty( $meta->key ) && ! empty( $meta->value ) && ( current_user_can( 'add_post_meta', $post_id, $unslashed_meta_key ) ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) { add_post_meta( $post_id, $meta->key, $meta->value ); } break; case 'update': if ( ! isset( $meta->value ) ) { break; } elseif ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_existing_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_metadata_by_mid( 'post', $meta->id, $meta->value ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value ); } break; } } } /** This action is documented in json-endpoints/class.wpcom-json-api-update-post-endpoint.php */ do_action( 'rest_api_inserted_post', $post_id, $insert, $new ); $return = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } if ( isset( $input['type'] ) && 'revision' === $input['type'] ) { $return['preview_nonce'] = wp_create_nonce( 'post_preview_' . $input['parent'] ); } if ( isset( $sticky ) ) { // workaround for sticky test occasionally failing, maybe a race condition with stick_post() above. $return['sticky'] = ( true === $sticky ); } if ( ! empty( $media_results['errors'] ) ) { /* * Depending on whether the errors array keys are sequential or not * json_encode would transform this into either an array or an object * see https://www.php.net/manual/en/function.json-encode.php#example-3967 * We use array_values to always return an array */ $return['media_errors'] = array_values( $media_results['errors'] ); } if ( 'publish' !== $return['status'] && isset( $input['title'] ) ) { $sal_site = $this->get_sal_post_by( 'ID', $post_id, $args['context'] ); $return['other_URLs'] = (object) $sal_site->get_permalink_suggestions( $input['title'] ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); return $return; } /** * Determine if a theme's functions.php file should be loaded. * * @param int $post_id Post ID. * * @return bool */ protected function should_load_theme_functions( $post_id = null ) { if ( empty( $post_id ) ) { $input = $this->input( true ); $type = $input['type'] ?? null; } else { $type = get_post_type( $post_id ); } return ! empty( $type ) && ! in_array( $type, array( 'post', 'revision' ), true ); } /** * Filter for rest_api_allowed_public_metadata. * Adds FB's DTP specific metadata. * * @param array $keys Array of metadata that is accessible by the REST API. * * @return array */ public function dtp_fb_allowed_metadata( $keys ) { return array_merge( $keys, array( '_dtp_fb', '_dtp_fb_geo_points', '_dtp_fb_post_link' ) ); } /** * Pre-process FB DTP posts before inserting. * Here we can improve the DTP content for the following issues: * - Render the map block based on provided coordinates in metadata * - [TODO] Improve the title * * @param array $post Post to be inserted. * @param array $metadata Metadata for the post. * * @return mixed */ private function dtp_fb_preprocess_post( $post, $metadata ) { $geo_points_metadata = wp_filter_object_list( $metadata, array( 'key' => '_dtp_fb_geo_points' ), 'and', 'value' ); if ( ! empty( $geo_points_metadata ) ) { $fb_points = reset( $geo_points_metadata ); $geo_points = array(); // Prepare Geo Points so that they match the format expected by the map block. foreach ( $fb_points as $fb_point ) { $geo_points[] = array( 'coordinates' => array( 'longitude' => $fb_point['longitude'], 'latitude' => $fb_point['latitude'], ), 'title' => $fb_point['name'], ); } if ( ! function_exists( 'map_block_from_geo_points' ) ) { require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/map/map.php'; } $map_block = map_block_from_geo_points( $geo_points ); $post['post_content'] = $map_block . $post['post_content']; } $post['post_format'] = 'aside'; return $post; } /** * Determines if a post exists based on title, content, date, and type, * But excluding IDs in gallery shortcodes. * This will prevent duplication of posts created through the API. * * @param string $title Post title. * @param string $content Post content. * @param string $post_date Post date. * @param string $type Optional post type. * @return int Post ID if post exists, 0 otherwise. */ private function post_exists( $title, $content, $post_date, $type = '' ) { $date = date_create( $post_date ); $posts = get_posts( array( 'year' => date_format( $date, 'Y' ), 'monthnum' => date_format( $date, 'n' ), 'day' => date_format( $date, 'j' ), 'hour' => date_format( $date, 'G' ), 'minute' => date_format( $date, 'i' ), 'second' => date_format( $date, 's' ), 'post_type' => $type, 'title' => $title, 'numberposts' => -1, 'suppress_filters' => false, ) ); foreach ( $posts as $post ) { $gallery_ids_pattern = "/(\[gallery[^\]]*)(\sids='[\d,]+')([^\]]*\])/"; $post->post_content = preg_replace( $gallery_ids_pattern, '$1$3', $post->post_content ); $content = preg_replace( $gallery_ids_pattern, '$1$3', $content ); if ( $content === $post->post_content ) { return $post->ID; } } return 0; } } class.wpcom-json-api-delete-media-v1-1-endpoint.php 0000644 00000007004 14722054026 0016044 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Delete_Media_v1_1_Endpoint( array( 'description' => 'Delete a piece of media. Note: Media is deleted and not trashed.', 'group' => 'media', 'stat' => 'media:1:delete', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/media/%d/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The media ID', ), 'response_format' => array( 'status' => '(string) Returns deleted if the media was successfully deleted', 'ID' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'post_ID' => '(int) ID of the post this media is attached to', 'author_ID' => '(int) ID of the user who uploaded the media', 'URL' => '(string) URL to the file', 'guid' => '(string) Unique identifier', 'file' => '(string) File name', 'extension' => '(string) File extension', 'mime_type' => '(string) File mime type', 'title' => '(string) File name', 'caption' => '(string) User-provided caption of the file', 'description' => '(string) Description of the file', 'alt' => '(string) Alternative text for image files.', 'thumbnails' => '(object) Media item thumbnail URL options', 'height' => '(int) (Image & video only) Height of the media item', 'width' => '(int) (Image & video only) Width of the media item', 'length' => '(int) (Video & audio only) Duration of the media item, in seconds', 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item', 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress', 'videopress_processing_done' => '(bool) (Video only) If the video is Uuploaded on a blog with VideoPress, this will return the status of processing on the Video', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/$media_ID/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Delete media v1_1 endpoint class. */ class WPCOM_JSON_API_Delete_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { //phpcs:ignore /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'delete_post', $media_id ) ) { return new WP_Error( 'unauthorized', 'User is not authorized delete media', 403 ); } $item = $this->get_media_item_v1_1( $media_id ); if ( is_wp_error( $item ) ) { return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); } wp_delete_post( $media_id, true ); $item->status = 'deleted'; return $item; } } class.wpcom-json-api-get-term-endpoint.php 0000644 00000005022 14722054026 0014565 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Term_Endpoint( array( 'description' => 'Get information about a single term.', 'group' => 'taxonomy', 'stat' => 'terms:1', 'method' => 'GET', 'path' => '/sites/%s/taxonomies/%s/terms/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$taxonomy' => '(string) Taxonomy', '$slug' => '(string) Term slug', ), 'response_format' => array( 'ID' => '(int) The term ID.', 'name' => '(string) The name of the term.', 'slug' => '(string) The slug of the term.', 'description' => '(string) The description of the term.', 'post_count' => '(int) The number of posts using this term.', 'parent' => '(int) The parent ID for the term, if hierarchical.', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/taxonomies/post_tag/terms/slug:wordpresscom', ) ); /** * GET Term endpoint class. */ class WPCOM_JSON_API_Get_Term_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * * /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param string $taxonomy - the taxonomy type. * @param int $slug - the slug. */ public function callback( $path = '', $blog_id = 0, $taxonomy = 'category', $slug = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } $taxonomy_meta = get_taxonomy( $taxonomy ); if ( false === $taxonomy_meta || ( ! $taxonomy_meta->public && ! current_user_can( $taxonomy_meta->cap->assign_terms ) ) ) { return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 ); } $args = $this->query_args(); $term = $this->get_taxonomy( $slug, $taxonomy, $args['context'] ); if ( ! $term || is_wp_error( $term ) ) { return $term; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'terms' ); return $term; } } class.wpcom-json-api-upload-media-endpoint.php 0000644 00000007324 14722054026 0015411 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Upload media item API endpoint. * * Endpoint: /sites/%s/media/new */ new WPCOM_JSON_API_Upload_Media_Endpoint( array( 'description' => 'Upload a new media item.', 'group' => 'media', 'stat' => 'media:new', 'method' => 'POST', 'path' => '/sites/%s/media/new', 'deprecated' => true, 'new_version' => '1.1', 'max_version' => '1', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'media' => '(media) An array of media to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Accepts images (image/gif, image/jpeg, image/png) only at this time.<br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", 'media_urls' => '(array) An array of URLs to upload to the post.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/new/', 'response_format' => array( 'media' => '(array) Array of uploaded media', 'errors' => '(array) Array of error messages of uploading media failures', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'media_urls' => 'https://s.w.org/about/images/logos/codeispoetry-rgb.png', ), ), ) ); /** * Upload media item API class. */ class WPCOM_JSON_API_Upload_Media_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Upload media item API endpoint callback. * * @param string $path API path. * @param int $blog_id Blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'upload_files' ) ) { return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 ); } $input = $this->input( true ); $has_media = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false; $has_media_urls = isset( $input['media_urls'] ) && $input['media_urls'] ? count( $input['media_urls'] ) : false; $errors = array(); $files = array(); $media_ids = array(); if ( $has_media ) { $this->api->trap_wp_die( 'upload_error' ); foreach ( $input['media'] as $index => $media_item ) { $_FILES['.api.media.item.'] = $media_item; // check for WP_Error if we ever actually need $media_id. $media_id = media_handle_upload( '.api.media.item.', 0 ); if ( is_wp_error( $media_id ) ) { if ( is_countable( $input['media'] ) && 1 === count( $input['media'] ) && ! $has_media_urls ) { unset( $_FILES['.api.media.item.'] ); return $media_id; } $errors[ $index ]['error'] = $media_id->get_error_code(); $errors[ $index ]['message'] = $media_id->get_error_message(); } else { $media_ids[ $index ] = $media_id; } $files[] = $media_item; } $this->api->trap_wp_die( null ); unset( $_FILES['.api.media.item.'] ); } if ( $has_media_urls ) { foreach ( $input['media_urls'] as $url ) { $id = $this->handle_media_sideload( $url ); if ( ! empty( $id ) && is_int( $id ) ) { $media_ids[] = $id; } } } $results = array(); foreach ( $media_ids as $media_id ) { $results[] = $this->get_media_item( $media_id ); } return array( 'media' => $results, 'errors' => $errors, ); } } class.wpcom-json-api-site-settings-v1-4-endpoint.php 0000644 00000034117 14722054026 0016337 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Site_Settings_V1_4_Endpoint( array( 'description' => 'Get detailed settings information about a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.4', 'method' => 'GET', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1.4/sites/en.blog.wordpress.com/settings?pretty=1', ) ); new WPCOM_JSON_API_Site_Settings_V1_4_Endpoint( array( 'description' => 'Update settings for a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.4', 'method' => 'POST', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'migration_source_site_domain' => '(string) The source site URL, from the migration flow', 'in_site_migration_flow' => '(string) Whether the site is currently in the Site Migration signup flow.', 'blogname' => '(string) Blog name', 'blogdescription' => '(string) Blog description', 'default_pingback_flag' => '(bool) Notify blogs linked from article?', 'default_ping_status' => '(bool) Allow link notifications from other blogs?', 'default_comment_status' => '(bool) Allow comments on new articles?', 'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines', 'wpcom_data_sharing_opt_out' => '(bool) Did the site opt out of public content sharing with third parties and research partners?', 'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati', 'jetpack_relatedposts_enabled' => '(bool) Enable related posts?', 'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?', 'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?', 'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?', 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?', 'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface', 'jetpack_search_enabled' => '(bool) Enable Jetpack Search', 'jetpack_search_supported' => '(bool) Jetpack Search supported', 'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow', 'infinite_scroll' => '(bool) Support infinite scroll of posts?', 'default_category' => '(int) Default post category', 'default_post_format' => '(string) Default post format', 'require_name_email' => '(bool) Require comment authors to fill out name and email?', 'comment_registration' => '(bool) Require users to be registered and logged in to comment?', 'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?', 'close_comments_days_old' => '(int) Age at which to close comments', 'thread_comments' => '(bool) Enable threaded comments?', 'thread_comments_depth' => '(int) Depth to thread comments', 'page_comments' => '(bool) Break comments into pages?', 'comments_per_page' => '(int) Number of comments to display per page', 'default_comments_page' => '(string) newest|oldest Which page of comments to display first', 'comment_order' => '(string) asc|desc Order to display comments within page', 'comments_notify' => '(bool) Email me when someone comments?', 'moderation_notify' => '(bool) Email me when a comment is helf for moderation?', 'social_notifications_like' => '(bool) Email me when someone likes my post?', 'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?', 'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?', 'comment_moderation' => '(bool) Moderate comments for manual approval?', 'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?', 'comment_max_links' => '(int) Moderate comments that contain X or more links', 'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line', 'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line', 'lang_id' => '(int) ID for language blog is written in', 'locale' => '(string) locale code for language blog is written in', 'site_vertical_id' => '(string) The site vertical ID', 'wga' => '(array) Google Analytics Settings', 'jetpack_cloudflare_analytics' => '(array) Cloudflare Analytics Settings', 'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?', 'disabled_reblogs' => '(bool) Are reblogs disabled on posts?', 'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?', 'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)', 'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"', 'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed', 'sharing_open_links' => '(string) Link target for sharing buttons (same or new)', 'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button', 'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.', 'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site', 'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'', 'gmt_offset' => '(int) Site offset from UTC in hours', 'date_format' => '(string) PHP Date-compatible date format', 'time_format' => '(string) PHP Date-compatible time format', 'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)', 'woocommerce_onboarding_profile' => '(array) woocommerce_onboarding_profile', 'woocommerce_store_address' => '(string) woocommerce_store_address option', 'woocommerce_store_address_2' => '(string) woocommerce_store_address_2 option', 'woocommerce_store_city' => '(string) woocommerce_store_city option', 'woocommerce_default_country' => '(string) woocommerce_default_country option', 'woocommerce_store_postcode' => '(string) woocommerce_store_postcode option', 'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site', 'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page', 'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site', 'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page', Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.', Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives', 'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook', 'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts', 'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear', 'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache', 'posts_per_page' => '(int) Number of posts to show on blog pages', 'posts_per_rss' => '(int) Number of posts to show in the RSS feed', 'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts', 'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts', 'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments', 'launchpad_screen' => '(string) Whether or not launchpad is presented and what size it will be', 'wpcom_featured_image_in_email' => '(bool) Whether the Featured image is displayed in the New Post email template or not', 'jetpack_gravatar_in_email' => '(bool) Whether to show author avatar in the email byline', 'jetpack_author_in_email' => '(bool) Whether to show author display name in the email byline', 'jetpack_post_date_in_email' => '(bool) Whether to show date in the email byline', 'wpcom_newsletter_categories' => '(array) Array of post category ids that are marked as newsletter categories', 'wpcom_newsletter_categories_enabled' => '(bool) Whether the newsletter categories are enabled or not', 'sm_enabled' => '(bool) Whether the newsletter Subscribe Modal is enabled or not', 'jetpack_subscribe_overlay_enabled' => '(bool) Whether the newsletter Subscribe Overlay is enabled or not', 'jetpack_subscribe_floating_button_enabled' => '(bool) Whether the newsletter floating subscribe button is enabled or not', 'jetpack_subscriptions_subscribe_post_end_enabled' => '(bool) Whether adding Subscribe block at the end of each post is enabled or not', 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled or not', 'jetpack_subscriptions_subscribe_navigation_enabled' => '(bool) Whether the Subscribe block navigation placement is enabled or not', 'wpcom_gifting_subscription' => '(bool) Whether gifting is enabled for non auto-renew sites', 'wpcom_reader_views_enabled' => '(bool) Whether showing post views in WordPress.com Reader is enabled for the site', 'wpcom_subscription_emails_use_excerpt' => '(bool) Whether site subscription emails (e.g. New Post email notification) will use post excerpts', 'jetpack_subscriptions_reply_to' => '(string) The reply to email behaviour for newsletter emails', 'jetpack_subscriptions_from_name' => '(string) The from name for newsletter emails', 'show_on_front' => '(string) Whether homepage should display related posts or a static page. The expected value is \'posts\' or \'page\'.', 'page_on_front' => '(string) The page ID of the page to use as the site\'s homepage. It will apply only if \'show_on_front\' is set to \'page\'.', 'page_for_posts' => '(string) The page ID of the page to use as the site\'s posts page. It will apply only if \'show_on_front\' is set to \'page\'.', 'subscription_options' => '(array) Array of three options used in subscription email templates: \'invitation\', \'welcome\' and \'comment_follow\' strings.', 'jetpack_verbum_subscription_modal' => '(bool) Whether Subscription modal is enabled in Verbum comments', 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt', 'enable_verbum_commenting' => '(bool) Whether Verbum commenting is enabled', 'enable_blocks_comments' => '(bool) Whether blocks comments are enabled', 'highlander_comment_form_prompt' => '(string) The prompt for the comment form', 'jetpack_comment_form_color_scheme' => '(string) The color scheme for the comment form', 'is_fully_managed_agency_site' => '(bool) Whether the site is a fully managed agency site', ), 'response_format' => array( 'updated' => '(array)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.4/sites/en.blog.wordpress.com/settings?pretty=1', ) ); /** * Settings v1_4 endpoint class. */ class WPCOM_JSON_API_Site_Settings_V1_4_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_3_Endpoint { /** * Get the defaults. * * @return array */ protected function get_defaults() { return array( 'code' => '', 'anonymize_ip' => false, 'honor_dnt' => false, 'ec_track_purchases' => false, 'ec_track_add_to_cart' => false, 'enh_ec_tracking' => false, 'enh_ec_track_remove_from_cart' => false, 'enh_ec_track_prod_impression' => false, 'enh_ec_track_prod_click' => false, 'enh_ec_track_prod_detail_view' => false, 'enh_ec_track_checkout_started' => false, ); } } class.wpcom-json-api-list-dropdown-pages-endpoint.php 0000644 00000013642 14722054026 0016752 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List dropdown pages endpoint. */ new WPCOM_JSON_API_List_Dropdown_Pages_Endpoint( array( 'description' => 'Get a list of pages to be displayed as options in a select-a-page-dropdown.', 'min_version' => '1.1', 'max_version' => '1.1', 'group' => 'posts', 'stat' => 'posts:dropdown-pages', 'method' => 'GET', 'path' => '/sites/%s/dropdown-pages/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/dropdown-pages/', ) ); /** * Endpoint class responsible for listing pages to be displayed as options in a select-a-page-dropdown. * * /sites/%s/dropdown-pages/ -> $blog_id */ class WPCOM_JSON_API_List_Dropdown_Pages_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Page object format. * * @var array */ public $dropdown_page_object_format = array( 'ID' => '(int) The page ID.', 'title' => '(string) The page title.', 'children' => '(array:dropdown_page) An array of child pages.', ); /** * The response format. * * @var array */ public $response_format = array( 'found' => '(int) The number of pages found.', 'dropdown_pages' => '(array:dropdown_page) An array of dropdown_page objects.', ); /** * List of pages indexed by their page ID. * * @var array<int,WP_Post> */ private $pages_by_id = array(); /** * List of pages indexed by their parent page ID. * * @var array<int,WP_Post> */ private $pages_by_parent = array(); /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @return stdClass[] $pages - An array of page objects. Each page object includes ID and title properties and may include children property. This makes each page object a tree-like data structure. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $pages = get_pages(); if ( empty( $pages ) ) { return array( 'found' => 0, 'dropdown_pages' => array(), ); } $this->pages_by_id = self::to_pages_by_id( $pages ); $this->pages_by_parent = self::to_pages_by_parent( $pages ); $dropdown_pages = $this->create_dropdown_pages(); return array( 'found' => count( $dropdown_pages ), 'dropdown_pages' => $dropdown_pages, ); } /** * Convert a list of pages to a list of pages by page ID. * * @param array<WP_Post> $pages - array of pages. * @return array<int,WP_Post> $pages_by_page_id - indexed array of pages by page ID where index is page ID. */ private static function to_pages_by_id( $pages ) { $pages_by_page_id = array(); foreach ( $pages as $page ) { if ( isset( $page->ID ) ) { $pages_by_page_id[ $page->ID ] = $page; } } return $pages_by_page_id; } /** * Convert a list of pages to a list of pages by parent. * * @param array<WP_Post> $pages - array of pages. * @return array<int,WP_Post> $pages_by_parent - indexed array of pages by parent where index is page ID. */ private static function to_pages_by_parent( $pages ) { $pages_by_parent = array(); foreach ( $pages as $page ) { if ( empty( $page->post_parent ) ) { $pages_by_parent['root'][] = $page; } else { $pages_by_parent[ $page->post_parent ][] = $page; } } return $pages_by_parent; } /** * Convert a list of pages to a list of dropdown pages. * * @return array<stdClass> $dropdown_pages - array of dropdown pages. */ private function create_dropdown_pages() { $dropdown_pages = array(); if ( ! empty( $this->pages_by_parent['root'] ) ) { foreach ( $this->pages_by_parent['root'] as $root_page ) { $dropdown_pages[] = $this->to_dropdown_page( $root_page ); } } if ( ! empty( $this->pages_by_id ) ) { // In case there were some orphans // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable foreach ( $this->pages_by_id as $_page_id => $page ) { $dropdown_pages[] = $this->to_dropdown_page( $page ); } } return $dropdown_pages; } /** * Convert a page to a dropdown page. * * @param WP_Post $page - the page. * @return stdClass|false $dropdown_page - the dropdown page. */ private function to_dropdown_page( $page ) { if ( ! isset( $page->ID ) ) { return false; } $title = $this->get_page_title( $page ); if ( ! isset( $this->pages_by_parent[ $page->ID ] ) ) { unset( $this->pages_by_id[ $page->ID ] ); return (object) array( 'ID' => $page->ID, 'title' => $title, ); } $children = array(); foreach ( $this->pages_by_parent[ $page->ID ] as $child_page ) { $children[] = $this->to_dropdown_page( $child_page ); } unset( $this->pages_by_id[ $page->ID ] ); unset( $this->pages_by_parent[ $page->ID ] ); return (object) array( 'ID' => $page->ID, 'title' => $title, 'children' => $children, ); } /** * Get the page title. * * @param WP_Post $page - the page. * @return string $page_title - the page title. */ private function get_page_title( $page ) { $title = $page->post_title; if ( '' === $title ) { /* translators: %d: ID of a post. */ $title = sprintf( __( '#%d (no title)', 'jetpack' ), $page->ID ); } /** * Filters the page title when creating an HTML drop-down list of pages. * * @since 3.1.0 * * @param string $title Page title. * @param WP_Post $page Page data object. */ $title = apply_filters( 'list_pages', $title, $page ); return $title; } } class.wpcom-json-api-get-autosave-v1-1-endpoint.php 0000644 00000005622 14722054026 0016135 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint * * @package automattic/jetpack */ new WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint( array( 'description' => 'Get the most recent autosave for a post.', 'group' => '__do_not_document', 'stat' => 'posts:autosave', 'min_version' => '1.1', 'method' => 'GET', 'path' => '/sites/%s/posts/%d/autosave', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'response_format' => array( 'ID' => '(int) autodraft post ID', 'post_ID' => '(int) post ID', 'author_ID' => '(int) author ID', 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) The post excerpt.', 'preview_URL' => '(string) preview URL for the post', 'modified' => '(ISO 8601 datetime) modified time', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/1/autosave', ) ); // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Class WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint */ class WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { /** * Get Autosave callback * /sites/%s/posts/%d/autosave -> $blog_id, $post_id * * @param string $path Path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. * * @return array|int|mixed|WP_Error */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } $autosave = wp_get_post_autosave( $post->ID ); if ( $autosave ) { $preview_url = add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ); $nonce = wp_create_nonce( 'post_preview_' . $post->ID ); $preview_url = add_query_arg( array( 'preview_id' => $post->ID, 'preview_nonce' => $nonce, ), $preview_url ); return array( 'ID' => $autosave->ID, 'author_ID' => $autosave->post_author, 'post_ID' => $autosave->post_parent, 'title' => $autosave->post_title, 'content' => $autosave->post_content, 'excerpt' => $autosave->post_excerpt, 'preview_URL' => $preview_url, 'modified' => $this->format_date( $autosave->post_modified_gmt, $autosave->post_modified ), ); } else { return new WP_Error( 'not_found', 'No autosaves exist for this post', 404 ); } } } class.wpcom-json-api-get-post-counts-v1-1-endpoint.php 0000644 00000012451 14722054026 0016602 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_GET_Post_Counts_V1_1_Endpoint( array( 'description' => 'Get number of posts in the post type groups by post status', 'group' => 'sites', 'stat' => 'sites:X:post-counts:X', 'force' => 'wpcom', 'method' => 'GET', 'min_version' => '1.1', 'max_version' => '1.2', 'path' => '/sites/%s/post-counts/%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_type' => '(string) Post Type', ), 'query_parameters' => array( 'context' => false, 'author' => '(int) author ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/post-counts/page', 'response_format' => array( 'counts' => array( 'all' => '(array) Number of posts by any author in the post type grouped by post status', 'mine' => '(array) Number of posts by the current user in the post type grouped by post status', ), ), ) ); /** * GET Post Counts v1_1 endpoint class. */ class WPCOM_JSON_API_GET_Post_Counts_V1_1_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Whitelist array. * * @var string[] */ private $allowlist = array( 'publish' ); /** * Build SQL query * * This function must `$wpdb->prepare` the query. The return is expected to be prepared by consuming functions. * * @param string $post_type - post type. * @param int $user_id - the user ID. * @return string SQL query */ private function buildCountsQuery( $post_type = 'post', $user_id = null ) { global $wpdb; $query = 'SELECT post_status as status, count(*) as count '; $query .= "FROM {$wpdb->posts} "; $query .= 'WHERE post_type = %s '; if ( isset( $user_id ) ) { $query .= 'AND post_author = %d '; } $query .= 'GROUP BY status'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This is properly prepared, except the query is constructed in the variable, throwing the PHPCS error. return $wpdb->prepare( $query, $post_type, $user_id ); } /** * Retrive counts using wp_cache * * @param string $post_type - thge post type. * @param int $id - the ID. */ private function retrieveCounts( $post_type, $id = null ) { if ( ! isset( $id ) ) { $counts = array(); foreach ( (array) wp_count_posts( $post_type ) as $status => $count ) { if ( in_array( $status, $this->allowlist, true ) && $count > 0 ) { $counts[ $status ] = (int) $count; } } return $counts; } global $wpdb; $key = 'rest-api-' . $id . '-' . _count_posts_cache_key( $post_type ); $counts = wp_cache_get( $key, 'counts' ); if ( false === $counts ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- buildCountsQuery prepares the query. $results = $wpdb->get_results( $this->buildCountsQuery( $post_type, $id ) ); $counts = $this->filterStatusesByWhiteslist( $results ); wp_cache_set( $key, $counts, 'counts' ); } return $counts; } /** * Filter statuses by whiteslist. * * @param array $in - the post we're checking. */ private function filterStatusesByWhiteslist( $in ) { $return = array(); foreach ( $in as $result ) { if ( in_array( $result->status, $this->allowlist, true ) ) { $return[ $result->status ] = (int) $result->count; } } return $return; } /** * * API callback. * * /sites/%s/post-counts/%s * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param string $post_type - the post type. */ public function callback( $path = '', $blog_id = 0, $post_type = 'post' ) { if ( ! get_current_user_id() ) { return new WP_Error( 'authorization_required', __( 'An active access token must be used to retrieve post counts.', 'jetpack' ), 403 ); } $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // @todo see if we can use a strict comparison here. // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict if ( ! in_array( $post_type, array( 'post', 'revision', 'page', 'any' ), true ) && defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } if ( ! post_type_exists( $post_type ) ) { return new WP_Error( 'unknown_post_type', __( 'Unknown post type requested.', 'jetpack' ), 404 ); } $args = $this->query_args(); $mine_ID = get_current_user_id(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase if ( current_user_can( 'edit_posts' ) ) { array_push( $this->allowlist, 'draft', 'future', 'pending', 'private', 'trash' ); } $return = array( 'counts' => (array) array( 'all' => (object) $this->retrieveCounts( $post_type ), 'mine' => (object) $this->retrieveCounts( $post_type, $mine_ID ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase ), ); // Author. if ( isset( $args['author'] ) ) { $author_ID = $args['author']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase $return['counts']['author'] = (object) $this->retrieveCounts( $post_type, $author_ID ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase } return (object) $return; } } class.wpcom-json-api-render-endpoint.php 0000644 00000012100 14722054026 0014313 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * These are helpers for the shortcode and embed render endpoints. */ abstract class WPCOM_JSON_API_Render_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Figure out what scripts and styles to load. * props to o2's o2_Read_API::poll() function for inspiration. * * In short we figure out what scripts load for a "normal" page load by executing wp_head and wp_footer * then we render the embed/shortcode (to both get our result, and to have the shortcode files enqueue their resources) * then we load wp_head and wp_footer again to see what new resources were added * finally we find out the url to the source file and any extra info (like media or init js) * * @param mixed $callback - the function callback. * @param mixed $callback_arg - the callback arguments. * * @return array */ public function process_render( $callback, $callback_arg ) { global $wp_scripts, $wp_styles; if ( false === defined( 'STYLESHEETPATH' ) ) { wp_templating_constants(); } // initial scripts & styles (to subtract) ob_start(); wp_head(); wp_footer(); ob_end_clean(); $initial_scripts = $wp_scripts->done; $initial_styles = $wp_styles->done; // actually render the shortcode, get the result, and do the resource loading again so we can subtract.. ob_start(); wp_head(); ob_end_clean(); $result = call_user_func( $callback, $callback_arg ); ob_start(); wp_footer(); ob_end_clean(); // find the difference (the new resource files) $loaded_scripts = array_diff( $wp_scripts->done, $initial_scripts ); $loaded_styles = array_diff( $wp_styles->done, $initial_styles ); return array( 'result' => $result, 'loaded_scripts' => $loaded_scripts, 'loaded_styles' => $loaded_styles, ); } /** * Takes the list of styles and scripts and adds them to the JSON response. * * @param array $return - what was returned. * @param array $loaded_scripts - the loaded scripts. * @param array $loaded_styles - the loaded styles. * * @return array */ public function add_assets( $return, $loaded_scripts, $loaded_styles ) { global $wp_scripts, $wp_styles; // scripts first, just cuz if ( $loaded_scripts !== array() ) { $scripts = array(); foreach ( $loaded_scripts as $handle ) { if ( ! isset( $wp_scripts->registered[ $handle ] ) ) { continue; } $src = $wp_scripts->registered[ $handle ]->src; // attach version and an extra query parameters $ver = $this->get_version( $wp_scripts->registered[ $handle ]->ver, $wp_scripts->default_version ); if ( isset( $wp_scripts->args[ $handle ] ) ) { $ver = $ver ? $ver . '&' . $wp_scripts->args[ $handle ] : $wp_scripts->args[ $handle ]; } $src = add_query_arg( 'ver', $ver, $src ); // add to an aray so we can return all this info $scripts[ $handle ] = array( 'src' => $src, ); $extra = $wp_scripts->print_extra_script( $handle, false ); if ( ! empty( $extra ) ) { $scripts[ $handle ]['extra'] = $extra; } } $return['scripts'] = $scripts; } // now styles if ( $loaded_styles !== array() ) { $styles = array(); foreach ( $loaded_styles as $handle ) { if ( ! isset( $wp_styles->registered[ $handle ] ) ) { continue; } $src = $wp_styles->registered[ $handle ]->src; // attach version and an extra query parameters $ver = $this->get_version( $wp_styles->registered[ $handle ]->ver, $wp_styles->default_version ); if ( isset( $wp_styles->args[ $handle ] ) ) { $ver = $ver ? $ver . '&' . $wp_styles->args[ $handle ] : $wp_styles->args[ $handle ]; } $src = add_query_arg( 'ver', $ver, $src ); // is there a special media (print, screen, etc) for this? if not, default to 'all' $media = 'all'; if ( isset( $wp_styles->registered[ $handle ]->args ) ) { $media = esc_attr( $wp_styles->registered[ $handle ]->args ); } // add to an array so we can return all this info $styles[ $handle ] = array( 'src' => $src, 'media' => $media, ); } $return['styles'] = $styles; } return $return; } /** * Returns the 'version' string set by the shortcode so different versions of scripts/styles can be loaded. * * @param string $this_scripts_version - this scripts version. * @param string $default_version - the default version. * * @return string */ public function get_version( $this_scripts_version, $default_version ) { if ( null === $this_scripts_version ) { $ver = ''; } else { $ver = $this_scripts_version ? $this_scripts_version : $default_version; } return $ver; } /** * Given a shortcode, process and return the result. * * @param string $shortcode - the shortcode. */ public function do_shortcode( $shortcode ) { return do_shortcode( $shortcode ); } /** * Given a one-line embed URL, process and return the result. * * @param string $embed_url - the embed URL. * * @return string|false */ public function do_embed( $embed_url ) { global $wp_embed; return $wp_embed->shortcode( array(), $embed_url ); } } class.wpcom-json-api-list-posts-v1-1-endpoint.php 0000644 00000047501 14722054026 0015654 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List posts v1_1 endpoint. */ new WPCOM_JSON_API_List_Posts_v1_1_Endpoint( array( 'description' => 'Get a list of matching posts.', 'min_version' => '1.1', 'max_version' => '1.1', 'group' => 'posts', 'stat' => 'posts', 'method' => 'GET', 'path' => '/sites/%s/posts/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'number' => '(int=20) The number of posts to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.', 'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.', 'order' => array( 'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.', 'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.', ), 'order_by' => array( 'date' => 'Order by the created time of each post.', 'modified' => 'Order by the modified time of each post.', 'title' => "Order lexicographically by the posts' titles.", 'comment_count' => 'Order by the number of comments for each post.', 'ID' => 'Order by post ID.', ), 'after' => '(ISO 8601 datetime) Return posts dated after the specified datetime.', 'before' => '(ISO 8601 datetime) Return posts dated before the specified datetime.', 'modified_after' => '(ISO 8601 datetime) Return posts modified after the specified datetime.', 'modified_before' => '(ISO 8601 datetime) Return posts modified before the specified datetime.', 'tag' => '(string) Specify the tag name or slug.', 'category' => '(string) Specify the category name or slug.', 'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.', 'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.', 'include' => '(array:int|int) Includes the specified post ID(s) in the response', 'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response', 'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.', 'status' => '(string) Comma-separated list of statuses for which to query, including any of: "publish", "private", "draft", "pending", "future", and "trash", or simply "any". Defaults to "publish"', 'sticky' => array( 'include' => 'Sticky posts are not excluded from the list.', 'exclude' => 'Sticky posts are excluded from the list.', 'require' => 'Only include sticky posts', ), 'author' => "(int) Author's user ID", 'search' => '(string) Search query', 'meta_key' => '(string) Metadata key that the post should contain', 'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/?number=2', ) ); /** * List Posts v1_1 Endpoint class. * * /sites/%s/posts/ -> $blog_id */ class WPCOM_JSON_API_List_Posts_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { // phpcs:ignore /** * Date range * * @var array */ public $date_range = array(); /** * Modified range * * @var array */ public $modified_range = array(); /** * Page handle * * @var array */ public $page_handle = array(); /** * Performed query * * @var array */ public $performed_query = null; /** * Response format. * * @var array */ public $response_format = array( 'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).', 'posts' => '(array:post) An array of post objects.', 'meta' => '(object) Meta data', ); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $is_eligible_for_page_handle = true; $site = $this->get_platform()->get_site( $blog_id ); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } if ( isset( $args['type'] ) && ! in_array( $args['type'], array( 'post', 'revision', 'page', 'any' ), true ) && defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } if ( isset( $args['type'] ) && ! $site->is_post_type_allowed( $args['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } // Normalize post_type. if ( isset( $args['type'] ) && 'any' === $args['type'] ) { if ( version_compare( $this->api->version, '1.1', '<' ) ) { $args['type'] = array( 'post', 'page' ); } else { // 1.1+ $args['type'] = $site->get_whitelisted_post_types(); } } // determine statuses. $status = ( ! empty( $args['status'] ) ) ? explode( ',', $args['status'] ) : array( 'publish' ); if ( is_user_logged_in() ) { $statuses_whitelist = array( 'publish', 'pending', 'draft', 'future', 'private', 'trash', 'any', ); $status = array_intersect( $status, $statuses_whitelist ); } else { // logged-out users can see only published posts. $statuses_whitelist = array( 'publish', 'any' ); $status = array_intersect( $status, $statuses_whitelist ); if ( empty( $status ) ) { // requested only protected statuses? nothing for you here. return array( 'found' => 0, 'posts' => array(), ); } // clear it (AKA published only) because "any" includes protected. $status = array(); } // let's be explicit about defaulting to 'post'. $args['type'] = isset( $args['type'] ) ? $args['type'] : 'post'; // make sure the user can read or edit the requested post type(s). if ( is_array( $args['type'] ) ) { $allowed_types = array(); foreach ( $args['type'] as $post_type ) { if ( $site->current_user_can_access_post_type( $post_type, $args['context'] ) ) { $allowed_types[] = $post_type; } } if ( empty( $allowed_types ) ) { return array( 'found' => 0, 'posts' => array(), ); } $args['type'] = $allowed_types; } elseif ( ! $site->current_user_can_access_post_type( $args['type'], $args['context'] ) ) { return array( 'found' => 0, 'posts' => array(), ); } $query = array( 'posts_per_page' => $args['number'], 'order' => $args['order'], 'orderby' => $args['order_by'], 'post_type' => $args['type'], 'post_status' => $status, 'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null, 'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null, 's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null, 'fields' => 'ids', ); if ( ! is_user_logged_in() ) { $query['has_password'] = false; } if ( isset( $args['include'] ) ) { $query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] ); } if ( isset( $args['meta_key'] ) ) { $show = false; if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) { $show = true; } if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) { $show = true; } if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) { return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 ); } $meta = array( 'key' => $args['meta_key'] ); if ( isset( $args['meta_value'] ) ) { $meta['value'] = $args['meta_value']; } $query['meta_query'] = array( $meta ); } if ( 'include' === $args['sticky'] ) { $query['ignore_sticky_posts'] = 1; } elseif ( 'exclude' === $args['sticky'] ) { $sticky = get_option( 'sticky_posts' ); if ( is_array( $sticky ) ) { $query['post__not_in'] = $sticky; } } elseif ( 'require' === $args['sticky'] ) { $sticky = get_option( 'sticky_posts' ); if ( is_array( $sticky ) && ! empty( $sticky ) ) { $query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky; } else { // no sticky posts exist. return array( 'found' => 0, 'posts' => array(), ); } } if ( isset( $args['exclude'] ) ) { $excluded_ids = (array) $args['exclude']; $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids; } if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) { // get_page_children is a misnomer; it supports all hierarchical post types. $page_args = array( 'child_of' => $args['exclude_tree'], 'post_type' => $args['type'], // since we're looking for things to exclude, be aggressive. 'post_status' => 'publish,draft,pending,private,future,trash', ); $post_descendants = get_pages( $page_args ); $exclude_tree = array( $args['exclude_tree'] ); foreach ( $post_descendants as $child ) { $exclude_tree[] = $child->ID; } $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree; } if ( isset( $args['category'] ) ) { $category = get_term_by( 'slug', $args['category'], 'category' ); if ( false === $category ) { $query['category_name'] = $args['category']; } else { $query['cat'] = $category->term_id; } } if ( isset( $args['tag'] ) ) { $query['tag'] = $args['tag']; } if ( ! empty( $args['term'] ) ) { $query['tax_query'] = array(); foreach ( $args['term'] as $taxonomy => $slug ) { $taxonomy_object = get_taxonomy( $taxonomy ); if ( false === $taxonomy_object || ( ! $taxonomy_object->public && ! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) { continue; } $query['tax_query'][] = array( 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => explode( ',', $slug ), ); } } if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $query['paged'] = $args['page']; if ( 1 !== $query['paged'] ) { $is_eligible_for_page_handle = false; } } else { if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $query['offset'] = $args['offset']; if ( 0 !== $query['offset'] ) { $is_eligible_for_page_handle = false; } } if ( isset( $args['before_gmt'] ) ) { $this->date_range['before'] = $args['before_gmt']; } if ( isset( $args['after_gmt'] ) ) { $this->date_range['after'] = $args['after_gmt']; } if ( isset( $args['modified_before_gmt'] ) ) { $this->modified_range['before'] = $args['modified_before_gmt']; } if ( isset( $args['modified_after_gmt'] ) ) { $this->modified_range['after'] = $args['modified_after_gmt']; } if ( $this->date_range ) { add_filter( 'posts_where', array( $this, 'handle_date_range' ) ); } if ( $this->modified_range ) { add_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); } if ( isset( $args['page_handle'] ) ) { $page_handle = wp_parse_args( $args['page_handle'] ); if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) { // we have a valid looking page handle. $this->page_handle = $page_handle; add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } } /** * 'column' necessary for the me/posts endpoint (which extends sites/$site/posts). * Would need to be added to the sites/$site/posts definition if we ever want to * use it there. */ $column_whitelist = array( 'post_modified_gmt' ); if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) { $query['column'] = $args['column']; } $this->performed_query = $query; add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); $wp_query = new WP_Query( $query ); remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); if ( $this->date_range ) { remove_filter( 'posts_where', array( $this, 'handle_date_range' ) ); $this->date_range = array(); } if ( $this->modified_range ) { remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); $this->modified_range = array(); } if ( $this->page_handle ) { remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } $return = array(); $excluded_count = 0; foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $return[ $key ] = (int) $wp_query->found_posts; break; case 'posts': $posts = array(); foreach ( $wp_query->posts as $post_ID ) { $the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] ); if ( $the_post && ! is_wp_error( $the_post ) ) { $posts[] = $the_post; } else { ++$excluded_count; } } if ( $posts ) { /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) ); } $return[ $key ] = $posts; break; case 'meta': if ( ! is_array( $args['type'] ) ) { $return[ $key ] = (object) array( 'links' => (object) array( 'counts' => (string) $this->links->get_site_link( $blog_id, 'post-counts/' . $args['type'] ), ), ); } if ( $is_eligible_for_page_handle && $return['posts'] ) { $last_post = end( $return['posts'] ); reset( $return['posts'] ); $post_count = is_countable( $return['posts'] ) ? count( $return['posts'] ) : 0; if ( ( $return['found'] > $post_count ) && $last_post ) { if ( ! isset( $return[ $key ] ) ) { $return[ $key ] = (object) array(); } $handle = $this->build_page_handle( $last_post, $query ); if ( $handle !== null ) { $return[ $key ]->next_page = $handle; } } } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( ! isset( $return[ $key ] ) ) { $return[ $key ] = new stdClass(); } $return[ $key ]->wpcom = true; } break; } } $return['found'] -= $excluded_count; return $return; } /** * Build the page handle. * * @param array $post - the post. * @param array $query - the query. */ public function build_page_handle( $post, $query ) { $column = $query['orderby']; if ( ! $column ) { $column = 'date'; } if ( ! isset( $post['ID'] ) || ! isset( $post[ $column ] ) ) { return null; } return build_query( array( 'value' => rawurlencode( $post[ $column ] ), 'id' => $post['ID'], ) ); } /** * Build the date range query. * * @param string $column - the database column. * @param array $range - the date range. * @param string $where - sql where clause. */ public function build_date_range_query( $column, $range, $where ) { global $wpdb; switch ( count( $range ) ) { case 2: $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['after'], $range['before'] ); break; case 1: if ( isset( $range['before'] ) ) { $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['before'] ); } else { $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['after'] ); } break; } return $where; } /** * Handle date range. * * @param string $where - sql where clause. */ public function handle_date_range( $where ) { return $this->build_date_range_query( 'post_date_gmt', $this->date_range, $where ); } /** * Handle modified date range. * * @param string $where - sql where clause. */ public function handle_modified_range( $where ) { return $this->build_date_range_query( 'post_modified_gmt', $this->modified_range, $where ); } /** * Handle where clause for page handle. * * @param string $where - sql where clause. */ public function handle_where_for_page_handle( $where ) { global $wpdb; $column = $this->performed_query['orderby']; if ( ! $column ) { $column = 'date'; } $order = $this->performed_query['order']; if ( ! $order ) { $order = 'DESC'; } if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ), true ) ) { return $where; } if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) { return $where; } $db_column = ''; $db_value = ''; switch ( $column ) { case 'ID': $db_column = 'ID'; $db_value = '%d'; break; case 'title': $db_column = 'post_title'; $db_value = '%s'; break; case 'date': $db_column = 'post_date'; $db_value = 'CAST( %s as DATETIME )'; break; case 'modified': $db_column = 'post_modified'; $db_value = 'CAST( %s as DATETIME )'; break; case 'comment_count': $db_column = 'comment_count'; $db_value = '%d'; break; } if ( 'DESC' === $order ) { $db_order = '<'; } else { $db_order = '>'; } // Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item // but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items // beyond the passed item. $where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( 'ID' !== $db_column ) { $where .= $wpdb->prepare( "OR ( `$wpdb->posts`.`$db_column` = $db_value AND `$wpdb->posts`.ID $db_order %d )", $this->page_handle['value'], $this->page_handle['id'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber } $where .= ' )'; return $where; } /** * Handle how the page handle is ordered. * * @param string $orderby - what we're ordering by. */ public function handle_orderby_for_page_handle( $orderby ) { global $wpdb; if ( 'ID' === $this->performed_query['orderby'] ) { // bail if we're already ordering by ID. return $orderby; } if ( $orderby ) { $orderby .= ' ,'; } $order = $this->performed_query['order']; if ( ! $order ) { $order = 'DESC'; } $orderby .= " `$wpdb->posts`.ID $order"; return $orderby; } } class.wpcom-json-api-list-media-v1-2-endpoint.php 0000644 00000006503 14722054026 0015561 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php'; /** * List media v1_2 endpoint. */ new WPCOM_JSON_API_List_Media_v1_2_Endpoint( array( 'description' => 'Get a list of items in the media library.', 'group' => 'media', 'stat' => 'media', 'min_version' => '1.2', 'max_version' => '1.2', 'method' => 'GET', 'path' => '/sites/%s/media/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=20) The number of media items to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.', 'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.', 'order' => array( 'DESC' => 'Return files in descending order. For dates, that means newest to oldest.', 'ASC' => 'Return files in ascending order. For dates, that means oldest to newest.', ), 'order_by' => array( 'date' => 'Order by the uploaded time of each file.', 'title' => 'Order lexicographically by file titles.', 'ID' => 'Order by media ID.', ), 'search' => '(string) Search query.', 'post_ID' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.', 'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).", 'after' => '(ISO 8601 datetime) Return media items uploaded after the specified datetime.', 'before' => '(ISO 8601 datetime) Return media items uploaded before the specified datetime.', ), 'response_format' => array( 'media' => '(array) Array of media objects', 'found' => '(int) The number of total results found', 'meta' => '(object) Meta data', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List Media v1_2 endpoint. */ class WPCOM_JSON_API_List_Media_v1_2_Endpoint extends WPCOM_JSON_API_List_Media_v1_1_Endpoint { // phpcs:ignore /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $response = parent::callback( $path, $blog_id ); if ( is_wp_error( $response ) ) { return $response; } $media_list = $response['media']; if ( ! is_countable( $media_list ) || count( $media_list ) === array() ) { return $response; } foreach ( $media_list as $media_item ) { // expose `revision_history` object for each image. $media_item->revision_history = (object) array( 'items' => (array) Jetpack_Media::get_revision_history( $media_item->ID ), 'original' => (object) Jetpack_Media::get_original_media( $media_item->ID ), ); } return $response; } } class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php 0000644 00000012277 14722054026 0017075 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Comments_Tree_v1_2_Endpoint( array( 'description' => 'Get a comments tree for site.', 'min_version' => '1.2', 'max_version' => '1.2', 'group' => 'comments-tree', 'stat' => 'comments-tree:1', 'method' => 'GET', 'path' => '/sites/%s/comments-tree', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'post_id' => '(int) Filter returned comments by a post.', 'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, pending, trash, spam).', ), 'response_format' => array( 'comments_tree' => '(array) Array of post IDs representing the comments tree for given site or post (max 50000)', 'trackbacks_tree' => '(array) Array of post IDs representing the trackbacks tree for given site or post (max 50000)', 'pingbacks_tree' => '(array) Array of post IDs representing the pingbacks tree for given site or post (max 50000)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/comments-tree?&status=approved&post_id=123', ) ); /** * Get comments tree v1_2 endpoint class. */ class WPCOM_JSON_API_Get_Comments_Tree_v1_2_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint { // phpcs:ignore /** * Retrieves a list of comment data. * * @param array $args { * Optional. Arguments to control behavior. Default empty array. * * @type int $max_comment_count Maximum number of comments returned. * @type int $post_id Filter by post. * @type int $start_at First comment to search from going back in time. * @type string $status Filter by status: all, approved, pending, spam or trash. * } * * @return array */ public function get_site_tree_v1_2( $args = array() ) { global $wpdb; $defaults = array( 'max_comment_count' => 50000, 'post_id' => null, 'start_at' => PHP_INT_MAX, 'status' => 'all', ); $args = wp_parse_args( $args, $defaults ); $db_status = $this->get_comment_db_status( $args['status'] ); if ( ! empty( $args['post_id'] ) ) { $db_comment_rows = $wpdb->get_results( $wpdb->prepare( 'SELECT comment_ID, comment_parent, comment_type ' . "FROM $wpdb->comments AS comments " . "WHERE comment_ID <= %d AND comment_post_ID = %d AND ( %s = 'all' OR comment_approved = %s ) " . 'ORDER BY comment_ID DESC ' . 'LIMIT %d', (int) $args['start_at'], (int) $args['post_id'], $db_status, $db_status, $args['max_comment_count'] ), ARRAY_N ); } else { $db_comment_rows = $wpdb->get_results( $wpdb->prepare( 'SELECT comment_ID, comment_parent, comment_type, comment_post_ID ' . "FROM $wpdb->comments AS comments " . "INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " . "WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " . 'ORDER BY comment_ID DESC ' . 'LIMIT %d', (int) $args['start_at'], $db_status, $db_status, $args['max_comment_count'] ), ARRAY_N ); } $comments = array(); $trackbacks = array(); $pingbacks = array(); foreach ( $db_comment_rows as $row ) { $comment_id = (int) $row[0]; $comment_parent_id = (int) $row[1]; $comment_post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : (int) $row[3]; if ( ! isset( $comments[ $comment_post_id ] ) ) { $comments[ $comment_post_id ] = array( array(), array() ); } switch ( $row[2] ) { case 'trackback': $trackbacks[ $comment_post_id ][] = $comment_id; break; case 'pingback': $pingbacks[ $comment_post_id ][] = $comment_id; break; default: if ( 0 === $comment_parent_id ) { $comments[ $comment_post_id ][0][] = $comment_id; } else { $comments[ $comment_post_id ][1][] = array( $comment_id, $comment_parent_id ); } } } return array( 'comments_tree' => $comments, 'trackbacks_tree' => $trackbacks, 'pingbacks_tree' => $pingbacks, ); } /** * Endpoint callback for /sites/%s/comments-tree * * @param string $path - the path. * @param int $blog_id - the blog ID. * * @return array Site or post tree results by status. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $filters = array(); if ( ! empty( $args['status'] ) ) { if ( ! $this->validate_status_param( $args['status'] ) ) { return new WP_Error( 'invalid_status', 'Invalid comment status value provided: ' . $args['status'] . '.', 400 ); } $filters['status'] = $args['status']; } if ( ! empty( $args['post_id'] ) ) { if ( get_post( absint( $args['post_id'] ) ) === null ) { return new WP_Error( 'invalid_post', 'Invalid post', 400 ); } $filters['post_id'] = absint( $args['post_id'] ); } return $this->get_site_tree_v1_2( $filters ); } } class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php 0000644 00000007052 14722054026 0017067 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint( array( 'description' => 'Get a comments tree for site.', 'min_version' => '1.1', 'max_version' => '1.1', 'group' => 'comments-tree', 'stat' => 'comments-tree:1', 'method' => 'GET', 'path' => '/sites/%s/comments-tree', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, pending, trash, spam).', ), 'response_format' => array( 'comments_count' => '(int) Total number of comments on the site', 'comments_tree' => '(array) Array of post IDs representing the comments tree for given site (max 50000)', 'trackbacks_count' => '(int) Total number of trackbacks on the site', 'trackbacks_tree' => '(array) Array of post IDs representing the trackbacks tree for given site (max 50000)', 'pingbacks_count' => '(int) Total number of pingbacks on the site', 'pingbacks_tree' => '(array) Array of post IDs representing the pingbacks tree for given site (max 50000)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/comments-tree?status=approved', ) ); /** * GET comments tree v1_1 endpoint. */ class WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_Endpoint { // phpcs:ignore /** * Retrieves a list of comment data for a given site. * * @param string $status Filter by status: all, approved, pending, spam or trash. * @param int $start_at first comment to search from going back in time. * * @return array */ public function get_site_tree( $status, $start_at = PHP_INT_MAX ) { global $wpdb; $max_comment_count = 50000; $db_status = $this->get_comment_db_status( $status ); $db_comment_rows = $wpdb->get_results( $wpdb->prepare( 'SELECT comment_ID, comment_post_ID, comment_parent, comment_type ' . "FROM $wpdb->comments AS comments " . "INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " . "WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " . 'ORDER BY comment_ID DESC ' . 'LIMIT %d', (int) $start_at, $db_status, $db_status, $max_comment_count ), ARRAY_N ); $comments = array(); $trackbacks = array(); $pingbacks = array(); foreach ( $db_comment_rows as $row ) { $comment_id = (int) $row[0]; $comment_post_id = (int) $row[1]; $comment_parent_id = (int) $row[2]; if ( ! isset( $comments[ $comment_post_id ] ) ) { $comments[ $comment_post_id ] = array( array(), array() ); } switch ( $row[3] ) { case 'trackback': $trackbacks[ $comment_post_id ][] = $comment_id; break; case 'pingback': $pingbacks[ $comment_post_id ][] = $comment_id; break; default: if ( 0 === $comment_parent_id ) { $comments[ $comment_post_id ][0][] = $comment_id; } else { $comments[ $comment_post_id ][1][] = array( $comment_id, $comment_parent_id ); } } } return array( 'comments_count' => $this->get_site_tree_total_count( $status, 'comment' ), 'comments_tree' => $comments, 'trackbacks_count' => $this->get_site_tree_total_count( $status, 'trackback' ), 'trackbacks_tree' => $trackbacks, 'pingbacks_count' => $this->get_site_tree_total_count( $status, 'pingback' ), 'pingbacks_tree' => $pingbacks, ); } } class.wpcom-json-api-delete-media-endpoint.php 0000644 00000004415 14722054026 0015365 0 ustar 00 <?php // phpcs:ignore Squiz.Commenting.FileComment.Missing new WPCOM_JSON_API_Delete_Media_Endpoint( array( 'description' => 'Delete a piece of media.', 'group' => 'media', 'stat' => 'media:1:delete', 'method' => 'POST', 'path' => '/sites/%s/media/%d/delete', 'deprecated' => true, 'new_version' => '1.1', 'max_version' => '1', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The media ID', ), 'response_format' => array( 'status' => '(string) Returns deleted if the media was successfully deleted', 'id' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'parent' => '(int) ID of the post this media is attached to', 'link' => '(string) URL to the file', 'title' => '(string) File name', 'caption' => '(string) User provided caption of the file', 'description' => '(string) Description of the file', 'metadata' => '(array) Misc array of information about the file, such as exif data or sizes', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/$media_ID/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Delete media endpoint class. */ class WPCOM_JSON_API_Delete_Media_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'delete_post', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $item = $this->get_media_item( $media_id ); if ( is_wp_error( $item ) ) { return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); } wp_delete_post( $media_id ); $item->status = 'deleted'; return $item; } } class.wpcom-json-api-get-site-v1-2-endpoint.php 0000644 00000014302 14722054026 0015246 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_GET_Site_V1_2_Endpoint( array( 'description' => 'Get information about a site.', 'group' => 'sites', 'stat' => 'sites:X', 'allowed_if_flagged' => true, 'method' => 'GET', 'min_version' => '1.2', 'path' => '/sites/%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'context' => false, 'filters' => '(string) Optional. Returns sites that satisfy the given filters only. Example: filters=jetpack,atomic,wpcom', ), 'response_format' => WPCOM_JSON_API_GET_Site_V1_2_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/', ) ); /** * GET Site v1_2 endpoint. */ class WPCOM_JSON_API_GET_Site_V1_2_Endpoint extends WPCOM_JSON_API_GET_Site_Endpoint { /** * Site format array. * * @var array $site_format */ public static $site_format = array( 'ID' => '(int) Site ID', 'name' => '(string) Title of site', 'description' => '(string) Tagline or description of site', 'URL' => '(string) Full URL to the site', 'capabilities' => '(array) Array of capabilities for the current user on this site.', 'jetpack' => '(bool) Whether the site is a Jetpack site or not', 'jetpack_connection' => '(bool) Whether the site is connected to WP.com via `jetpack-connection`', 'is_multisite' => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.', 'site_owner' => '(int) User ID of the site owner', 'post_count' => '(int) The number of posts the site has', 'subscribers_count' => '(int) The number of subscribers the site has', 'locale' => '(string) Primary locale code of the site', 'icon' => '(array) An array of icon formats for the site', 'logo' => '(array) The site logo, set in the Customizer', 'visible' => '(bool) If this site is visible in the user\'s site list', 'is_private' => '(bool) If the site is a private site or not', 'is_coming_soon' => '(bool) If the site is a "coming soon" site or not', 'single_user_site' => '(bool) Whether the site is single user. Only returned for WP.com sites and for Jetpack sites with version 3.4 or higher.', 'is_vip' => '(bool) If the site is a VIP site or not.', 'is_following' => '(bool) If the current user is subscribed to this site in the reader', 'organization_id' => '(int) P2 Organization identifier.', 'options' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site. Note: Post formats is deprecated, please see /sites/$id/post-formats/', 'plan' => '(array) Details of the current plan for this site.', 'products' => '(array) Details of the current products for this site.', 'zendesk_site_meta' => '(array) Site meta data for Zendesk.', 'updates' => '(array) An array of available updates for plugins, themes, wordpress, and languages.', 'jetpack_modules' => '(array) A list of active Jetpack modules.', 'meta' => '(object) Meta data', 'quota' => '(array) An array describing how much space a user has left for uploads', 'launch_status' => '(string) A string describing the launch status of a site', 'site_migration' => '(array) Data about any migration into the site.', 'is_fse_active' => '(bool) If the site has Full Site Editing active or not.', 'is_fse_eligible' => '(bool) If the site is capable of Full Site Editing or not', 'is_core_site_editor_enabled' => '(bool) If the site has the core site editor enabled.', 'is_wpcom_atomic' => '(bool) If the site is a WP.com Atomic one.', 'is_wpcom_staging_site' => '(bool) If the site is a WP.com staging site.', 'was_ecommerce_trial' => '(bool) If the site ever used an eCommerce trial.', 'was_upgraded_from_trial' => '(bool) If the site ever upgraded to a paid plan from a trial.', 'was_migration_trial' => '(bool) If the site ever used a migration trial.', 'was_hosting_trial' => '(bool) If the site ever used a hosting trial.', 'is_deleted' => '(bool) If the site flagged as deleted.', 'is_a4a_client' => '(bool) If the site is an A4A client site.', 'is_a4a_dev_site' => '(bool) If the site is an A4A dev site.', ); /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { add_filter( 'sites_site_format', array( $this, 'site_format' ) ); // Site filtering is a WPCOM concept, once a request gets anywhere else it should just be returned if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { // Apply filter here, return same error as switch_to_blog_and_validate_user if blog is not found. require_lib( 'site-filter' ); $filters = Site_Filter::process_query_arg( $this->query_args() ); if ( is_wp_error( $filters ) ) { return $filters; } if ( ! empty( $filters ) && ! Site_Filter::filter_blog( $this->api->get_blog_id( $blog_id ), $filters ) ) { return new WP_Error( 'unknown_blog', 'Unknown blog', 404 ); } } return parent::callback( $path, $blog_id ); } /** * Site format. * * @param string $format - the format. */ public function site_format( $format ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable return self::$site_format; } } class.wpcom-json-api-edit-media-v1-2-endpoint.php 0000644 00000043327 14722054026 0015540 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php'; define( 'REVISION_HISTORY_MAXIMUM_AMOUNT', 5 ); define( 'WP_ATTACHMENT_IMAGE_ALT', '_wp_attachment_image_alt' ); new WPCOM_JSON_API_Edit_Media_v1_2_Endpoint( array( 'description' => 'Edit a media item.', 'group' => 'media', 'stat' => 'media:1:POST', 'min_version' => '1', 'max_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/media/%d/edit', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'request_format' => array( 'parent_id' => '(int) ID of the post this media is attached to', 'title' => '(string) The file name.', 'caption' => '(string) File caption.', 'description' => '(HTML) Description of the file.', 'alt' => '(string) Alternative text for image files.', 'artist' => '(string) Audio Only. Artist metadata for the audio track.', 'album' => '(string) Audio Only. Album metadata for the audio track.', 'media' => '(media) An object file to attach to the post. To upload media, ' . 'the entire request should be multipart/form-data encoded. ' . 'Multiple media items will be displayed in a gallery. Accepts ' . 'jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. ' . 'Audio and Video may also be available. See <code>allowed_file_types</code> ' . 'in the options response of the site endpoint. ' . '<br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image' \<br />--form 'media=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", 'attrs' => '(object) An Object of attributes (`title`, `description` and `caption`) ' . 'are supported to assign to the media uploaded via the `media` or `media_url`', 'media_url' => '(string) An URL of the image to attach to a post.', ), 'response_format' => array( 'ID' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'post_ID' => '(int) ID of the post this media is attached to', 'author_ID' => '(int) ID of the user who uploaded the media', 'URL' => '(string) URL to the file', 'guid' => '(string) Unique identifier', 'file' => '(string) File name', 'extension' => '(string) File extension', 'mime_type' => '(string) File mime type', 'title' => '(string) File name', 'caption' => '(string) User provided caption of the file', 'description' => '(string) Description of the file', 'alt' => '(string) Alternative text for image files.', 'thumbnails' => '(object) Media item thumbnail URL options', 'height' => '(int) (Image & video only) Height of the media item', 'width' => '(int) (Image & video only) Width of the media item', 'length' => '(int) (Video & audio only) Duration of the media item, in seconds', 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item', 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress', 'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.', 'revision_history' => '(object) An object with `items` and `original` keys. ' . '`original` is an object with data about the original image. ' . '`items` is an array of snapshots of the previous images of this Media. ' . 'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/446', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Updated Title', ), ), ) ); /** * Edit media v1_2 endpoint class. */ class WPCOM_JSON_API_Edit_Media_v1_2_Endpoint extends WPCOM_JSON_API_Update_Media_v1_1_Endpoint { //phpcs:ignore /** * Return an array of mime_type items allowed when the media file is uploaded. * * @param array $default_mime_types - array of default mime types. * * @return array mime_type array */ public static function get_allowed_mime_types( $default_mime_types ) { return array_unique( array_merge( $default_mime_types, array( 'application/msword', // .doc 'application/vnd.ms-powerpoint', // .ppt, .pps 'application/vnd.ms-excel', // .xls 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx 'application/vnd.oasis.opendocument.text', // .odt 'application/pdf', // .pdf ) ) ); } /** * Update the media post grabbing the post values from * the `attrs` parameter * * @param int $media_id - post media ID. * @param array $attrs - `attrs` parameter sent from the client in the request body. */ private function update_by_attrs_parameter( $media_id, $attrs ) { $post_update_action = null; $insert = array(); // Attributes: Title, Caption, Description. if ( isset( $attrs['title'] ) ) { $insert['post_title'] = $attrs['title']; } if ( isset( $attrs['caption'] ) ) { $insert['post_excerpt'] = $attrs['caption']; } if ( isset( $attrs['description'] ) ) { $insert['post_content'] = $attrs['description']; } if ( ! empty( $insert ) ) { $insert['ID'] = $media_id; $update_action = wp_update_post( (object) $insert ); if ( is_wp_error( $update_action ) ) { return $update_action; } } // Attributes: Alt. if ( isset( $attrs['alt'] ) ) { $alt = wp_strip_all_tags( $attrs['alt'], true ); $post_update_action = update_post_meta( $media_id, WP_ATTACHMENT_IMAGE_ALT, $alt ); if ( is_wp_error( $post_update_action ) ) { return $post_update_action; } } // Attributes: Artist, Album. $id3_meta = array(); foreach ( array( 'artist', 'album' ) as $key ) { if ( isset( $attrs[ $key ] ) ) { $id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true ); } } if ( ! empty( $id3_meta ) ) { // Before updating metadata, ensure that the item is audio. $item = $this->get_media_item_v1_1( $media_id ); if ( str_starts_with( $item->mime_type, 'audio/' ) ) { $update_action = wp_update_attachment_metadata( $media_id, $id3_meta ); if ( is_wp_error( $update_action ) ) { return $update_action; } } } return $post_update_action; } /** * Return an object to be used to store into the revision_history * * @param object $media_item - media post object. * @return object the snapshot object */ private function get_snapshot( $media_item ) { $current_file = get_attached_file( $media_item->ID ); $file_paths = pathinfo( $current_file ); $snapshot = array( 'date' => (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified ), 'URL' => (string) wp_get_attachment_url( $media_item->ID ), 'file' => (string) $file_paths['basename'], 'extension' => (string) $file_paths['extension'], 'mime_type' => (string) $media_item->post_mime_type, 'size' => (int) filesize( $current_file ), ); return (object) $snapshot; } /** * Try to remove the temporal file from the given file array. * * @param array $file_array - Array with data about the temporal file. */ private function remove_tmp_file( $file_array ) { if ( file_exists( $file_array['tmp_name'] ) ) { wp_delete_file( $file_array['tmp_name'] ); } } /** * Save the given temporal file in a local folder. * * @param array $file_array - array containing file data. * @param int $media_id - the media id. * @param bool $is_upload - True if `$file_array` derives from an upload in `$_FILES`, false if this is a sideload. * @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong. */ private function save_temporary_file( $file_array, $media_id, $is_upload ) { $tmp_filename = $file_array['tmp_name']; $is_ok = $is_upload ? is_uploaded_file( $tmp_filename ) : file_exists( $tmp_filename ); if ( ! $is_ok ) { return new WP_Error( 'invalid_input', 'No media provided in input.' ); } // add additional mime_types through of the `jetpack_supported_media_sideload_types` filter. $mime_type_static_filter = array( 'WPCOM_JSON_API_Edit_Media_v1_2_Endpoint', 'get_allowed_mime_types', ); add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter ); if ( ! $this->is_file_supported_for_sideloading( $tmp_filename ) && ! file_is_displayable_image( $tmp_filename ) ) { if ( ! $is_upload ) { wp_delete_file( $tmp_filename ); } return new WP_Error( 'invalid_input', 'Invalid file type.', 403 ); } remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter ); // generate a new file name. $tmp_new_filename = Jetpack_Media::generate_new_filename( $media_id, $file_array['name'] ); // start to create the parameters to move the temporal file. $overrides = array( 'test_form' => false ); $time = $this->get_time_string_from_guid( $media_id ); $file_array['name'] = $tmp_new_filename; if ( $is_upload ) { $file = wp_handle_upload( $file_array, $overrides, $time ); } else { $file = wp_handle_sideload( $file_array, $overrides, $time ); $this->remove_tmp_file( $file_array ); } if ( isset( $file['error'] ) ) { return new WP_Error( 'upload_error', $file['error'] ); } return $file; } /** * File urls use the post date to generate a folder path. * Post dates can change, so we use the original date used in the guid * url so edits can remain in the same folder. In the following function * we capture a string in the format of `YYYY/MM` from the guid. * * For example with a guid of * "http://test.files.wordpress.com/2016/10/test.png" the resulting string * would be: "2016/10" * * @param int $media_id - the media id. * * @return string */ private function get_time_string_from_guid( $media_id ) { // @todo: investigate if we can replace date with gmdate() // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $time = date( 'Y/m', strtotime( current_time( 'mysql' ) ) ); $media = get_post( $media_id ); if ( $media ) { $pattern = '/\/(\d{4}\/\d{2})\//'; preg_match( $pattern, $media->guid, $matches ); if ( count( $matches ) > 1 ) { $time = $matches[1]; } } return $time; } /** * Get the image from a remote url and then save it locally. * * @param int $media_id - media post ID. * @param string $url - image URL to save locally. * @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong. */ private function build_file_array_from_url( $media_id, $url ) { if ( ! $url ) { return null; } // if we didn't get a URL, let's bail. $parsed = wp_parse_url( $url ); if ( empty( $parsed ) ) { return new WP_Error( 'invalid_url', 'No media provided in url.' ); } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $url = wpcom_get_private_file( $url ); } // save the remote image into a tmp file. $tmp = download_url( $url ); if ( is_wp_error( $tmp ) ) { return $tmp; } return array( 'name' => basename( $url ), 'tmp_name' => $tmp, ); } /** * Add a new item into revision_history array. * * @param object $media_item - media post. * @param array|WP_Error $file - File data, or WP_Error on error. * @param bool $has_original_media - condition is the original media has been already added. * @return bool `true` if the item has been added. Otherwise `false`. */ private function register_revision( $media_item, $file, $has_original_media ) { if ( is_wp_error( $file ) || ! $has_original_media ) { return false; } add_post_meta( $media_item->ID, Jetpack_Media::WP_REVISION_HISTORY, $this->get_snapshot( $media_item ) ); } /** * Restore the original media file. * * @param int $media_id - media post ID. * @param object $original_media - orginal media data. * @return array - restore media info. */ private function restore_original( $media_id, $original_media ) { $revisions = (array) Jetpack_Media::get_revision_history( $media_id ); $revisions = array_filter( $revisions, function ( $revision ) use ( $original_media ) { return $revision->file !== $original_media->file; } ); $criteria = array( 'from' => 0, 'to' => REVISION_HISTORY_MAXIMUM_AMOUNT, ); Jetpack_Media::remove_items_from_revision_history( $media_id, $criteria, $revisions ); $file = get_attached_file( $media_id ); $file_parts = pathinfo( $file ); $orginal_file = path_join( $file_parts['dirname'], $original_media->file ); $restored_media = array( 'file' => $orginal_file, 'type' => $original_media->mime_type, ); return $restored_media; } /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $media_item = get_post( $media_id ); if ( ! $media_item || is_wp_error( $media_item ) ) { return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); } if ( is_wp_error( $media_item ) ) { return $media_item; } if ( ! current_user_can( 'upload_files', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $input = $this->input( true ); // Images. $media_file = isset( $input['media'] ) ? (array) $input['media'] : null; $media_url = isset( $input['media_url'] ) ? $input['media_url'] : null; $media_attrs = isset( $input['attrs'] ) ? (array) $input['attrs'] : null; if ( isset( $media_url ) || $media_file ) { $user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token(); if ( ! $user_can_upload_files ) { return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 ); } $has_original_media = Jetpack_Media::get_original_media( $media_id ); if ( ! $has_original_media ) { // The first time that the media is updated // the original media is stored into the revision_history. $snapshot = $this->get_snapshot( $media_item ); add_post_meta( $media_id, Jetpack_Media::WP_ORIGINAL_MEDIA, $snapshot, true ); } // save the temporal file locally. $is_upload = (bool) $media_file; $temporal_file = $media_file ? $media_file : $this->build_file_array_from_url( $media_id, $media_url ); if ( is_wp_error( $temporal_file ) ) { return $temporal_file; } // edited media is sent as $media_file and restored media is sent as $media_url $should_restore = isset( $media_url ) && ! isset( $media_file ) && $has_original_media; $uploaded_file = $should_restore ? $this->restore_original( $media_id, $has_original_media ) : $this->save_temporary_file( $temporal_file, $media_id, $is_upload ); if ( is_wp_error( $uploaded_file ) ) { return $uploaded_file; } // revision_history control. $this->register_revision( $media_item, $uploaded_file, $has_original_media ); $uploaded_path = $uploaded_file['file']; $udpated_mime_type = $uploaded_file['type']; $was_updated = update_attached_file( $media_id, $uploaded_path ); if ( $was_updated ) { $new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path ); wp_update_attachment_metadata( $media_id, $new_metadata ); // check maximum amount of revision_history. Jetpack_Media::limit_revision_history( $media_id, REVISION_HISTORY_MAXIMUM_AMOUNT ); wp_update_post( (object) array( 'ID' => $media_id, 'post_mime_type' => $udpated_mime_type, ) ); } unset( $input['media'] ); unset( $input['media_url'] ); unset( $input['attrs'] ); } // update media through of `attrs` value it it's defined. if ( ( $media_file || isset( $media_url ) ) && $media_attrs ) { $was_updated = $this->update_by_attrs_parameter( $media_id, $media_attrs ); if ( is_wp_error( $was_updated ) ) { return $was_updated; } } // call parent method. $response = parent::callback( $path, $blog_id, $media_id ); // expose `revision_history` object. $response->revision_history = (object) array( 'items' => (array) Jetpack_Media::get_revision_history( $media_id ), 'original' => (object) Jetpack_Media::get_original_media( $media_id ), ); return $response; } } class.wpcom-json-api-get-post-endpoint.php 0000644 00000006777 14722054026 0014625 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoints: /sites/%s/posts/%d -> $blog_id, $post_id * /sites/%s/posts/name:%s -> $blog_id, $post_id // not documented * /sites/%s/posts/slug:%s -> $blog_id, $post_id */ new WPCOM_JSON_API_Get_Post_Endpoint( array( 'description' => 'Get a single post (by ID).', 'group' => 'posts', 'stat' => 'posts:1', 'new_version' => '1.1', 'max_version' => '1', 'method' => 'GET', 'path' => '/sites/%s/posts/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7', ) ); new WPCOM_JSON_API_Get_Post_Endpoint( array( 'description' => 'Get a single post (by name)', 'group' => '__do_not_document', 'stat' => 'posts:name', 'method' => 'GET', 'path' => '/sites/%s/posts/name:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_name' => '(string) The post name (a.k.a. slug)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/name:blogging-and-stuff', ) ); new WPCOM_JSON_API_Get_Post_Endpoint( array( 'description' => 'Get a single post (by slug).', 'group' => 'posts', 'stat' => 'posts:slug', 'new_version' => '1.1', 'max_version' => '1', 'method' => 'GET', 'path' => '/sites/%s/posts/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_slug' => '(string) The post slug (a.k.a. sanitized name)', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff', ) ); /** * Get post endpoint class. */ class WPCOM_JSON_API_Get_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $post_id - the post ID. */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); if ( ! str_contains( $path, '/posts/slug:' ) && ! str_contains( $path, '/posts/name:' ) ) { $get_by = 'ID'; } else { $get_by = 'name'; } $return = $this->get_post_by( $get_by, $post_id, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } if ( ! $this->current_user_can_access_post_type( $return['type'], $args['context'] ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); return $return; } } class.wpcom-json-api-get-comments-tree-endpoint.php 0000644 00000014552 14722054026 0016410 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Comments_Tree_Endpoint( array( 'description' => 'Get a comments tree for site.', 'max_version' => '1', 'new_version' => '1.1', 'group' => 'comments-tree', 'stat' => 'comments-tree:1', 'method' => 'GET', 'path' => '/sites/%s/comments-tree', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, unapproved, pending, trash, spam).', ), 'response_format' => array( 'comments_count' => '(int) Total number of comments on the site', 'comments_tree' => '(array) Array of arrays representing the comments tree for given site (max 50000)', 'trackbacks_count' => '(int) Total number of trackbacks on the site', 'trackbacks_tree' => '(array) Array of arrays representing the trackbacks tree for given site (max 50000)', 'pingbacks_count' => '(int) Total number of pingbacks on the site', 'pingbacks_tree' => '(array) Array of arrays representing the pingbacks tree for given site (max 50000)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments-tree?status=approved', ) ); /** * GET comments tree endpoint class. */ class WPCOM_JSON_API_Get_Comments_Tree_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Retrieves a list of comment data for a given site. * * @param string $status Filter by status: all, approved, pending, spam or trash. * @param int $start_at first comment to search from going back in time. * * @return array */ public function get_site_tree( $status, $start_at = PHP_INT_MAX ) { global $wpdb; $max_comment_count = 50000; $db_status = $this->get_comment_db_status( $status ); $db_comment_rows = $wpdb->get_results( $wpdb->prepare( 'SELECT comment_ID, comment_post_ID, comment_parent, comment_type ' . "FROM $wpdb->comments AS comments " . "INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " . "WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " . 'ORDER BY comment_ID DESC ' . 'LIMIT %d', (int) $start_at, $db_status, $db_status, $max_comment_count ), ARRAY_N ); $comments = array(); $trackbacks = array(); $pingbacks = array(); foreach ( $db_comment_rows as $row ) { list( $comment_id, $comment_post_id, $comment_parent, $comment_type ) = $row; switch ( $comment_type ) { case 'trackback': $trackbacks[] = array( $comment_id, $comment_post_id, $comment_parent ); break; case 'pingback': $pingbacks[] = array( $comment_id, $comment_post_id, $comment_parent ); break; default: $comments[] = array( $comment_id, $comment_post_id, $comment_parent ); } } return array( 'comments_count' => $this->get_site_tree_total_count( $status, 'comment' ), 'comments_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $comments ), 'trackbacks_count' => $this->get_site_tree_total_count( $status, 'trackback' ), 'trackbacks_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $trackbacks ), 'pingbacks_count' => $this->get_site_tree_total_count( $status, 'pingback' ), 'pingbacks_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $pingbacks ), ); } /** * Ensure all values are integers. * * @param array $comments Collection of comments. * * @return array Comments with values as integers. */ public function array_map_all_as_ints( $comments ) { return array_map( 'intval', $comments ); } /** * Retrieves a total count of comments by type for the given site. * * @param string $status Filter by status: all, approved, pending, spam or trash. * @param string $type Comment type: 'trackback', 'pingback', or 'comment'. * * @return int Total count of comments for a site. */ public function get_site_tree_total_count( $status, $type ) { global $wpdb; $db_status = $this->get_comment_db_status( $status ); $type = $this->get_sanitized_comment_type( $type ); $result = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(1) ' . "FROM $wpdb->comments AS comments " . "INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " . "WHERE comment_type = %s AND ( %s = 'all' OR comment_approved = %s )", $type, $db_status, $db_status ) ); return (int) $result; } /** * Ensure a valid status is converted to a database-supported value if necessary. * * @param string $status Should be one of: all, approved, pending, spam or trash. * * @return string Corresponding value that exists in database. */ public function get_comment_db_status( $status ) { if ( 'approved' === $status ) { return '1'; } if ( 'pending' === $status || 'unapproved' === $status ) { return '0'; } return $status; } /** * Determine if the passed comment status is valid or not. * * @param string $status - comment status. * * @return boolean */ public function validate_status_param( $status ) { return in_array( $status, array( 'all', 'approved', 'unapproved', 'pending', 'spam', 'trash' ), true ); } /** * Sanitize a given comment type. * * @param string $type Comment type: can be 'trackback', 'pingback', or 'comment'. * * @return string Sanitized comment type. */ public function get_sanitized_comment_type( $type = 'comment' ) { if ( in_array( $type, array( 'trackback', 'pingback', 'comment' ), true ) ) { return $type; } return 'comment'; } /** * Endpoint callback for /sites/%s/comments-tree * * @param string $path - the api path. * @param int $blog_id - the blog id. * * @return array Site tree results by status. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $comment_status = empty( $args['status'] ) ? 'all' : $args['status']; if ( ! $this->validate_status_param( $comment_status ) ) { return new WP_Error( 'invalid_status', "Invalid comment status value provided: '$comment_status'.", 400 ); } return $this->get_site_tree( $comment_status ); } } class.wpcom-json-api-autosave-post-v1-1-endpoint.php 0000644 00000010177 14722054026 0016344 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint * * @package automattic/jetpack */ new WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint( array( 'description' => 'Create a post autosave.', 'group' => '__do_not_document', 'stat' => 'posts:autosave', 'min_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d/autosave', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'request_format' => array( 'content' => '(HTML) The post content.', 'title' => '(HTML) The post title.', 'excerpt' => '(HTML) The post excerpt.', ), 'response_format' => array( 'ID' => '(int) autodraft post ID', 'post_ID' => '(int) post ID', 'preview_URL' => '(string) preview URL for the post', 'modified' => '(ISO 8601 datetime) modified time', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/1/autosave', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Howdy', 'content' => 'Hello. I am a test post. I was created by the API', ), ), ) ); // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Class WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint */ class WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { /** * Autosave Post callback. * /sites/%s/posts/%d/autosave -> $blog_id, $post_id * * @param string $path Path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { if ( ! defined( 'DOING_AUTOSAVE' ) ) { define( 'DOING_AUTOSAVE', true ); } $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { // Make sure Custom Post Types, etc. get registered. $this->load_theme_functions(); } $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } $post_data = array( 'post_ID' => $post_id, 'post_type' => $post->post_type, 'post_title' => $input['title'], 'post_content' => $input['content'], 'post_excerpt' => $input['excerpt'], ); $preview_url = add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ); if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() === (int) $post->post_author && ( 'auto-draft' === $post->post_status || 'draft' === $post->post_status ) ) { // Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked. $auto_id = edit_post( wp_slash( $post_data ) ); } else { // Non drafts or other users drafts are not overwritten. The autosave is stored in a special post revision for each user. $auto_id = wp_create_post_autosave( wp_slash( $post_data ) ); $nonce = wp_create_nonce( 'post_preview_' . $post->ID ); $preview_url = add_query_arg( array( 'preview_id' => $post->ID, 'preview_nonce' => $nonce, ), $preview_url ); } $updated_post = get_post( $auto_id ); if ( $updated_post && $updated_post->ID && $updated_post->post_modified ) { return array( 'ID' => $auto_id, 'post_ID' => $post->ID, 'modified' => $this->format_date( $updated_post->post_modified_gmt, $updated_post->post_modified ), 'preview_URL' => $preview_url, ); } else { return new WP_Error( 'autosave_error', __( 'Autosave encountered an unexpected error', 'jetpack' ), 500 ); } } } class.wpcom-json-api-menus-v1-1-endpoint.php 0000644 00000074563 14722054026 0014672 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound /** * Menus abstract endpoint class. */ abstract class WPCOM_JSON_API_Menus_Abstract_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Switch to blog and validate user. * * @param string $site - the site we want to validate. * * @return int */ protected function switch_to_blog_and_validate_user( $site ) { $site_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( 'unauthorised', 'User cannot edit theme options on this site.', 403 ); } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } return $site_id; } /** * Get the locations of the menus. * * @return array[] */ protected function get_locations() { $locations = array(); $menus = get_registered_nav_menus(); if ( ! empty( $menus ) ) { foreach ( $menus as $name => $description ) { $locations[] = array( 'name' => $name, 'description' => $description, ); } } $locations = array_merge( $locations, WPCOM_JSON_API_Menus_Widgets::get() ); // Primary (first) location should have defaultState -> default, // all other locations (including widgets) should have defaultState -> empty. for ( $i = 0, $l = count( $locations ); $i < $l; $i++ ) { $locations[ $i ]['defaultState'] = $i ? 'empty' : 'default'; } return $locations; } /** * Simplify the menus. * * @param WP_Term|WP_Term[] $data - the menus we're simplifying. * @return array|array[] Simplified menu data. */ protected function simplify( $data ) { $simplifier = new WPCOM_JSON_API_Menus_Simplifier( $data ); return $simplifier->translate(); } /** * Complexify the menus. * * @param array[] $data - the menu data we're complexifying. * @return array[]|WP_Error Complexified menu data, or WP_Error on error. */ protected function complexify( $data ) { $complexifier = new WPCOM_JSON_API_Menus_Complexify( $data ); return $complexifier->translate(); } } /** * The menu translator class. */ abstract class WPCOM_JSON_API_Menus_Translator { /** * A string identifying this class. * * @var string */ protected $filter = ''; /** * List of filter method names. * * Filter methods are passed an array, and return a transformed array or WP_Error. * * @var array */ protected $filters = array(); /** * Class constructor. * * @param mixed $menus - a menu or list of menus. */ public function __construct( $menus ) { $this->is_single_menu = ! is_array( $menus ); $this->menus = is_array( $menus ) ? $menus : array( $menus ); } /** * Translate the menus. * * @return array|array[]|WP_Error */ public function translate() { $result = $this->menus; foreach ( $this->filters as $f ) { $result = call_user_func( array( $this, $f ), $result ); if ( is_wp_error( $result ) ) { return $result; } } return $this->maybe_extract( $result ); } /** * Return a single menu or an array of menus. * * @param array $menus - the menu list. * * @return array|array[] */ protected function maybe_extract( $menus ) { return $this->is_single_menu ? $menus[0] : $menus; } /** * See if we need to whitelist and rename. * * @param object|array $object - the object (or associative array) we're checking. * @param array $dict Associative array holding the key whitelist and renaming/casting data. * Keys are the keys from $object` to preserve. Values are the key to use in the output or an * assoc where 'name' specifies the output key and 'type' specifies the PHP type to cast the value to. * * @return array */ public function whitelist_and_rename_with( $object, $dict ) { $return = array(); foreach ( (array) $object as $k => $v ) { if ( isset( $dict[ $k ] ) ) { if ( is_array( $dict[ $k ] ) ) { settype( $v, $dict[ $k ]['type'] ); $return[ $dict[ $k ]['name'] ] = $v; } else { $new_k = $dict[ $k ]; $return[ $new_k ] = $v; } } } return $return; } } /** * The simplifier class. */ class WPCOM_JSON_API_Menus_Simplifier extends WPCOM_JSON_API_Menus_Translator { /** * The simplify translator class. * * @var string */ protected $filter = 'wpcom_menu_api_translator_simplify'; /** * The simplify filters. * * @var array */ protected $filters = array( 'whitelist_and_rename_keys', 'add_locations', 'treeify', 'add_widget_locations', ); /** * The menu whitelist. * * @var array */ protected $menu_whitelist = array( 'term_id' => array( 'name' => 'id', 'type' => 'int', ), 'name' => array( 'name' => 'name', 'type' => 'string', ), 'description' => array( 'name' => 'description', 'type' => 'string', ), 'items' => array( 'name' => 'items', 'type' => 'array', ), ); /** * The menu item whitelist. * * @var array */ protected $menu_item_whitelist = array( 'db_id' => array( 'name' => 'id', 'type' => 'int', ), 'object_id' => array( 'name' => 'content_id', 'type' => 'int', ), 'object' => array( 'name' => 'type', 'type' => 'string', ), 'type' => array( 'name' => 'type_family', 'type' => 'string', ), 'type_label' => array( 'name' => 'type_label', 'type' => 'string', ), 'title' => array( 'name' => 'name', 'type' => 'string', ), 'menu_order' => array( 'name' => 'order', 'type' => 'int', ), 'menu_item_parent' => array( 'name' => 'parent', 'type' => 'int', ), 'url' => array( 'name' => 'url', 'type' => 'string', ), 'target' => array( 'name' => 'link_target', 'type' => 'string', ), 'attr_title' => array( 'name' => 'link_title', 'type' => 'string', ), 'description' => array( 'name' => 'description', 'type' => 'string', ), 'classes' => array( 'name' => 'classes', 'type' => 'array', ), 'xfn' => array( 'name' => 'xfn', 'type' => 'string', ), ); /************************** * Filters methods **************************/ /** * Treeify the menus. * * @param array $menus - the menu list. * * @return array */ public function treeify( $menus ) { return array_map( array( $this, 'treeify_menu' ), $menus ); } /** * Turn the flat item list into a tree of items. * * @param array $menu - the menu. * * @return array */ protected function treeify_menu( $menu ) { $indexed_nodes = array(); $tree = array(); foreach ( $menu['items'] as &$item ) { $indexed_nodes[ $item['id'] ] = &$item; } foreach ( $menu['items'] as &$item ) { if ( $item['parent'] && isset( $indexed_nodes[ $item['parent'] ] ) ) { $parent_node = &$indexed_nodes[ $item['parent'] ]; if ( ! isset( $parent_node['items'] ) ) { $parent_node['items'] = array(); } $parent_node['items'][ $item['order'] ] = &$item; } else { $tree[ $item['order'] ] = &$item; } unset( $item['order'] ); unset( $item['parent'] ); } $menu['items'] = $tree; $this->remove_item_keys( $menu ); return $menu; } /** * Recursively ensure item lists are contiguous. * * @param array $item - the item list. */ protected function remove_item_keys( &$item ) { if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) { return; } foreach ( $item['items'] as &$it ) { $this->remove_item_keys( $it ); } $item['items'] = array_values( $item['items'] ); } /** * Whitelist and rename keys. * * @param (object|array)[] $menus - the menu list. * * @return array[] */ protected function whitelist_and_rename_keys( $menus ) { $transformed_menus = array(); foreach ( $menus as $menu ) { $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist ); if ( isset( $menu['items'] ) ) { foreach ( $menu['items'] as &$item ) { $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist ); } } $transformed_menus[] = $menu; } return $transformed_menus; } /** * Add menu locations. * * @param array $menus - the menu list. * * @return array[] */ protected function add_locations( $menus ) { $menus_with_locations = array(); foreach ( $menus as $menu ) { $menu['locations'] = array_keys( get_nav_menu_locations(), $menu['id'] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict $menus_with_locations[] = $menu; } return $menus_with_locations; } /** * Add widget locations. * * @param array $menus - the menu list. * * @return array[] */ protected function add_widget_locations( $menus ) { $nav_menu_widgets = WPCOM_JSON_API_Menus_Widgets::get(); if ( ! is_array( $nav_menu_widgets ) ) { return $menus; } foreach ( $menus as &$menu ) { $widget_locations = array(); foreach ( $nav_menu_widgets as $key => $widget ) { if ( is_array( $widget ) && isset( $widget['nav_menu'] ) && $widget['nav_menu'] === $menu['id'] ) { $widget_locations[] = 'nav_menu_widget-' . $key; } } $menu['locations'] = array_merge( $menu['locations'], $widget_locations ); } return $menus; } } /** * Complexify menu class. */ class WPCOM_JSON_API_Menus_Complexify extends WPCOM_JSON_API_Menus_Translator { /** * The complexify filter. * * @var string */ protected $filter = 'wpcom_menu_api_translator_complexify'; /** * The filters. * * @var array */ protected $filters = array( 'untreeify', 'set_locations', 'whitelist_and_rename_keys', ); /** * The menu whitelist. * * @var array */ protected $menu_whitelist = array( 'id' => 'term_id', 'name' => 'menu-name', 'description' => 'description', 'items' => 'items', ); /** * The item whitelist. * * @var array */ protected $menu_item_whitelist = array( 'id' => 'menu-item-db-id', 'content_id' => 'menu-item-object-id', 'type' => 'menu-item-object', 'type_family' => 'menu-item-type', 'type_label' => 'menu-item-type-label', 'name' => 'menu-item-title', 'order' => 'menu-item-position', 'parent' => 'menu-item-parent-id', 'url' => 'menu-item-url', 'link_target' => 'menu-item-target', 'link_title' => 'menu-item-attr-title', 'status' => 'menu-item-status', 'tmp_id' => 'tmp_id', 'tmp_parent' => 'tmp_parent', 'description' => 'menu-item-description', 'classes' => 'menu-item-classes', 'xfn' => 'menu-item-xfn', ); /************************** * Filters methods **************************/ /** * Untreeify the menu. * * @param array $menus - the list of menus. * * @return array[] */ public function untreeify( $menus ) { return array_map( array( $this, 'untreeify_menu' ), $menus ); } /** * Convert the tree of menu items to a flat list suitable for the nav_menu APIs. * * @param array $menu - the menu we're untreeifying. * * @return array */ protected function untreeify_menu( $menu ) { if ( empty( $menu['items'] ) ) { return $menu; } $items_list = array(); $counter = 1; foreach ( $menu['items'] as &$item ) { $item['parent'] = 0; } $this->untreeify_items( $menu['items'], $items_list, $counter ); $menu['items'] = $items_list; return $menu; } /** * Recurse the items tree adding each item to a flat list and restoring * `order` and `parent` fields. * * @param array $items item tree. * @param array $items_list output flat list of items. * @param int $counter for creating temporary IDs. */ protected function untreeify_items( $items, &$items_list, &$counter ) { foreach ( $items as $index => $item ) { $item['order'] = $index + 1; if ( ! isset( $item['id'] ) ) { $this->set_tmp_id( $item, $counter++ ); } if ( isset( $item['items'] ) && is_array( $item['items'] ) ) { foreach ( $item['items'] as &$i ) { $i['parent'] = $item['id']; } $this->untreeify_items( $item['items'], $items_list, $counter ); unset( $item['items'] ); } $items_list[] = $item; } } /** * Populate `tmp_id` field for a new item, and `tmp_parent` field * for all its children, to maintain the hierarchy. * These fields will be used when creating * new items with wp_update_nav_menu_item(). * * @param array $item - the item tree. * @param string $tmp_id - the tmp ID. */ private function set_tmp_id( &$item, $tmp_id ) { $item['tmp_id'] = $tmp_id; if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) { return; } foreach ( $item['items'] as &$child ) { $child['tmp_parent'] = $tmp_id; } } /** * Whitelist and rename keys. * * @param array $menus - the menus. * * @return array[] */ protected function whitelist_and_rename_keys( $menus ) { $transformed_menus = array(); foreach ( $menus as $menu ) { $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist ); if ( isset( $menu['items'] ) ) { $menu['items'] = array_map( array( $this, 'whitelist_and_rename_item_keys' ), $menu['items'] ); } $transformed_menus[] = $menu; } return $transformed_menus; } /** * Whitelist and rename item keys. * * @param array $item - the item. * * @return array */ protected function whitelist_and_rename_item_keys( $item ) { $item = $this->implode_array_fields( $item ); $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist ); return $item; } /** * All item fields are set as strings. * * @param array $menu_item - the menu item. * @return array Item with fields imploded. */ protected function implode_array_fields( $menu_item ) { return array_map( array( $this, 'implode_array_field' ), $menu_item ); } /** * Implode an array field. * * @param mixed $field - the field we're imploding. * * @return mixed The imploded string if `$field` was an array, otherwise `$field` unchanged. */ protected function implode_array_field( $field ) { if ( is_array( $field ) ) { return implode( ' ', $field ); } return $field; } /** * Set the menu locations. * * @param array $menus - the menu list. * * @return array[]|WP_Error */ protected function set_locations( $menus ) { foreach ( $menus as $menu ) { if ( isset( $menu['locations'] ) ) { if ( true !== $this->locations_are_valid( $menu['locations'] ) ) { return $this->locations_are_valid( $menu['locations'] ); } } } return array_map( array( $this, 'set_location' ), $menus ); } /** * Set the menu locations. * * @param array $menu - the menu. * * @return array */ protected function set_location( $menu ) { $this->set_menu_at_locations( $menu['locations'], $menu['id'] ); return $menu; } /** * Set the menu at locations. * * @param array $locations - the locations. * @param int $menu_id - the menu ID. */ protected function set_menu_at_locations( $locations, $menu_id ) { $location_map = get_nav_menu_locations(); $this->remove_menu_from_all_locations( $menu_id, $location_map ); if ( is_array( $locations ) ) { foreach ( $locations as $location ) { $location_map[ $location ] = $menu_id; } } set_theme_mod( 'nav_menu_locations', $location_map ); $this->set_widget_menu_at_locations( $locations, $menu_id ); } /** * Remove from all locations. * * @param int $menu_id - the menu ID. * @param array $location_map - the location map. */ protected function remove_menu_from_all_locations( $menu_id, &$location_map ) { foreach ( get_nav_menu_locations() as $existing_location => $existing_menu_id ) { if ( $existing_menu_id === $menu_id ) { unset( $location_map[ $existing_location ] ); } } } /** * Set widget menu at locations. * * @param array $locations - the locations. * @param int $menu_id - the menu ID. */ protected function set_widget_menu_at_locations( $locations, $menu_id ) { $nav_menu_widgets = get_option( 'widget_nav_menu' ); if ( ! is_array( $nav_menu_widgets ) ) { return; } // Remove menus from all custom menu widget locations foreach ( $nav_menu_widgets as &$widget ) { if ( is_array( $widget ) && isset( $widget['nav_menu'] ) && $widget['nav_menu'] === $menu_id ) { $widget['nav_menu'] = 0; } } if ( is_array( $locations ) ) { foreach ( $locations as $location ) { if ( preg_match( '/^nav_menu_widget-(\d+)/', $location, $matches ) ) { if ( isset( $matches[1] ) ) { $nav_menu_widgets[ $matches[1] ]['nav_menu'] = $menu_id; } } } } update_option( 'widget_nav_menu', $nav_menu_widgets ); } /** * Check if the locations are valid. * * @param int|array $locations - the location we're checking. * * @return bool|WP_Error */ protected function locations_are_valid( $locations ) { if ( is_int( $locations ) ) { if ( $locations !== 0 ) { return new WP_Error( 'locations-int', 'Locations int must be 0.', 400 ); } else { return true; } } elseif ( is_array( $locations ) ) { foreach ( $locations as $location_name ) { if ( ! $this->location_name_exists( $location_name ) ) { return new WP_Error( 'locations-array', sprintf( "Location '%s' does not exist.", $location_name ), 404 ); } } return true; } return new WP_Error( 'locations', 'Locations must be array or integer.', 400 ); } /** * Check if the location name exists. * * @param string $location_name - the location name. * * @return bool */ protected function location_name_exists( $location_name ) { $widget_location_names = wp_list_pluck( WPCOM_JSON_API_Menus_Widgets::get(), 'name' ); $existing_locations = get_nav_menu_locations(); if ( ! is_array( get_registered_nav_menus() ) ) { return false; } return array_key_exists( $location_name, get_registered_nav_menus() ) || array_key_exists( $location_name, $existing_locations ) || in_array( $location_name, $widget_location_names, true ); } } new WPCOM_JSON_API_Menus_New_Menu_Endpoint( array( 'method' => 'POST', 'description' => 'Create a new navigation menu.', 'group' => 'menus', 'stat' => 'menus:new-menu', 'path' => '/sites/%s/menus/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'name' => '(string) Name of menu', ), 'response_format' => array( 'id' => '(int) Newly created menu ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/new', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'name' => 'Menu 1', ), ), ) ); /** * New menu endpoint class. */ class WPCOM_JSON_API_Menus_New_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint { /** * The API Callback. * * @param string $path - the path. * @param int $site - the site ID. * * @return array|WP_Error */ public function callback( $path = '', $site = 0 ) { $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } $data = $this->input(); $id = wp_create_nav_menu( $data['name'] ); if ( is_wp_error( $id ) ) { return $id; } return array( 'id' => $id ); } } new WPCOM_JSON_API_Menus_Update_Menu_Endpoint( array( 'method' => 'POST', 'description' => 'Update a navigation menu.', 'group' => 'menus', 'stat' => 'menus:update-menu', 'path' => '/sites/%s/menus/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$menu_id' => '(int) Menu ID', ), 'request_format' => array( 'name' => '(string) Name of menu', 'items' => '(array) A list of menu item objects. <br/><br/> Item objects contain fields relating to that item, e.g. id, type, content_id, but they can also contain other items objects - this nesting represents parents and child items in the item tree.', ), 'response_format' => array( 'menu' => '(object) Updated menu object', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'name' => 'Test Menu', ), ), ) ); /** * Update menu endpoint class. */ class WPCOM_JSON_API_Menus_Update_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint { /** * The API Callback. * * @param string $path - the path. * @param int $site - the site ID. * @param int $menu_id - the menu ID. * * @return array|WP_Error */ public function callback( $path = '', $site = 0, $menu_id = 0 ) { $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } if ( $menu_id <= 0 ) { return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 ); } $data = $this->input( true, false ); $data['id'] = $menu_id; $data = $this->complexify( array( $data ) ); if ( is_wp_error( $data ) ) { return $data; } $data = $data[0]; // Avoid special-case handling of an unset 'items' field in empty menus $data['items'] = isset( $data['items'] ) ? $data['items'] : array(); $data = $this->create_new_items( $data, $menu_id ); $result = wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $data['menu-name'] ) ); if ( is_wp_error( $result ) ) { return $result; } $delete_status = $this->delete_items_not_present( $menu_id, $data['items'] ); if ( is_wp_error( $delete_status ) ) { return $delete_status; } foreach ( $data['items'] as $item ) { $item_id = isset( $item['menu-item-db-id'] ) ? $item['menu-item-db-id'] : 0; $result = wp_update_nav_menu_item( $menu_id, $item_id, $item ); if ( is_wp_error( $result ) ) { return $result; } } $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) ); if ( is_wp_error( $items ) ) { return $items; } $menu = wp_get_nav_menu_object( $menu_id ); $menu->items = $items; return array( 'menu' => $this->simplify( $menu ) ); } /** * New items can have a 'tmp_id', allowing them to * be used as parent items before they have been created. * * This function will create items that have a 'tmp_id' set, and * update any items with a 'tmp_parent' to use the * newly created item as a parent. * * @param array $data - the data we're checking. * @param int $menu_id - the menu ID. * @return array `$data` with new item IDs filled in. */ public function create_new_items( $data, $menu_id ) { $tmp_to_actual_ids = array(); foreach ( $data['items'] as &$item ) { if ( isset( $item['tmp_id'] ) ) { $actual_id = wp_update_nav_menu_item( $menu_id, 0, $item ); $tmp_to_actual_ids[ $item['tmp_id'] ] = $actual_id; unset( $item['tmp_id'] ); $item['menu-item-db-id'] = $actual_id; } } foreach ( $data['items'] as &$item ) { if ( isset( $item['tmp_parent'] ) ) { $item['menu-item-parent-id'] = $tmp_to_actual_ids[ $item['tmp_parent'] ]; unset( $item['tmp_parent'] ); } } return $data; } /** * Remove any existing menu items not present in the supplied array. * returns wp_error if an item cannot be deleted. * * @param int $menu_id - the menu ID. * @param array $menu_items - the menu items. * * @return bool|WP_Error */ public function delete_items_not_present( $menu_id, $menu_items ) { $existing_items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) ); if ( ! is_array( $existing_items ) ) { return true; } $existing_ids = wp_list_pluck( $existing_items, 'db_id' ); $ids_to_keep = wp_list_pluck( $menu_items, 'menu-item-db-id' ); $ids_to_remove = array_diff( $existing_ids, $ids_to_keep ); foreach ( $ids_to_remove as $id ) { if ( false === wp_delete_post( $id, true ) ) { return new WP_Error( 'menu-item', sprintf( 'Failed to delete menu item with id: %d.', $id ), 400 ); } } return true; } } new WPCOM_JSON_API_Menus_List_Menus_Endpoint( array( 'method' => 'GET', 'description' => 'Get a list of all navigation menus.', 'group' => 'menus', 'stat' => 'menus:list-menu', 'path' => '/sites/%s/menus', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'response_format' => array( 'menus' => '(array) A list of menu objects.<br/><br/> A menu object contains a name, items, locations, etc. Check the example response for the full structure. <br/><br/> Item objects contain fields relating to that item, e.g. id, type, content_id, but they can also contain other items objects - this nesting represents parents and child items in the item tree.', 'locations' => '(array) Locations where menus can be placed. List of objects, one per location.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); /** * List menus endpoint class. */ class WPCOM_JSON_API_Menus_List_Menus_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint { /** * The API Callback. * * @param string $path - the path. * @param int $site - the site ID. * * @return array|WP_Error */ public function callback( $path = '', $site = 0 ) { $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } $menus = wp_get_nav_menus( array( 'orderby' => 'term_id' ) ); if ( is_wp_error( $menus ) ) { return $menus; } foreach ( $menus as $m ) { $items = wp_get_nav_menu_items( $m->term_id, array( 'update_post_term_cache' => false ) ); if ( is_wp_error( $items ) ) { return $items; } $m->items = $items; } $menus = $this->simplify( $menus ); if ( is_wp_error( $this->get_locations() ) ) { return $this->get_locations(); } return array( 'menus' => $menus, 'locations' => $this->get_locations(), ); } } new WPCOM_JSON_API_Menus_Get_Menu_Endpoint( array( 'method' => 'GET', 'description' => 'Get a single navigation menu.', 'group' => 'menus', 'stat' => 'menus:get-menu', 'path' => '/sites/%s/menus/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$menu_id' => '(int) Menu ID', ), 'response_format' => array( 'menu' => '(object) A menu object.<br/><br/> A menu object contains a name, items, locations, etc. Check the example response for the full structure. <br/><br/> Item objects contain fields relating to that item, e.g. id, type, content_id, but they can also contain other items objects - this nesting represents parents and child items in the item tree.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); /** * Get menu endpoint class. */ class WPCOM_JSON_API_Menus_Get_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint { /** * The API Callback. * * @param string $path - the path. * @param int $site - the site ID. * @param int $menu_id - the menu ID. * * @return array|WP_Error */ public function callback( $path = '', $site = 0, $menu_id = 0 ) { $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } if ( $menu_id <= 0 ) { return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 ); } $menu = get_term( $menu_id, 'nav_menu' ); if ( is_wp_error( $menu ) ) { return $menu; } $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) ); if ( is_wp_error( $items ) ) { return $items; } $menu->items = $items; return array( 'menu' => $this->simplify( $menu ) ); } } new WPCOM_JSON_API_Menus_Delete_Menu_Endpoint( array( 'method' => 'POST', 'description' => 'Delete a navigation menu', 'group' => 'menus', 'stat' => 'menus:delete-menu', 'path' => '/sites/%s/menus/%d/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$menu_id' => '(int) Menu ID', ), 'response_format' => array( 'deleted' => '(bool) Has the menu been deleted?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/$menu_id/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); /** * Delete menu endpoint class. */ class WPCOM_JSON_API_Menus_Delete_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint { /** * The API Callback. * * @param string $path - the path. * @param int $site - the site ID. * @param int $menu_id - the menu ID. * * @return array|WP_Error */ public function callback( $path = '', $site = 0, $menu_id = 0 ) { $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) ); if ( is_wp_error( $site_id ) ) { return $site_id; } if ( $menu_id <= 0 ) { return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 ); } $result = wp_delete_nav_menu( $menu_id ); if ( ! is_wp_error( $result ) ) { $result = array( 'deleted' => $result ); } return $result; } } /** * API Menus widgets class. */ class WPCOM_JSON_API_Menus_Widgets { /** * Get the menu locations. * * @return array */ public static function get() { $locations = array(); $nav_menu_widgets = get_option( 'widget_nav_menu' ); if ( ! is_array( $nav_menu_widgets ) ) { return $locations; } foreach ( $nav_menu_widgets as $k => $v ) { if ( is_array( $v ) && isset( $v['title'] ) ) { $locations[ $k ] = array( 'name' => 'nav_menu_widget-' . $k, 'description' => $v['title'], ); } } return $locations; } } class.wpcom-json-api-site-user-endpoint.php 0000644 00000020414 14722054026 0014763 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Site_User_Endpoint( array( 'description' => 'Get details of a user of a site by ID.', 'group' => '__do_not_document', // 'users' 'stat' => 'sites:1:user', 'method' => 'GET', 'path' => '/sites/%s/users/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$user_id' => '(int) User ID', ), 'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/23', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_response' => '{ "ID": 18342963, "login": "binarysmash", "email": false, "name": "binarysmash", "URL": "http:\/\/binarysmash.wordpress.com", "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G", "profile_URL": "http:\/\/gravatar.com\/binarysmash", "roles": [ "administrator" ] }', ) ); new WPCOM_JSON_API_Site_User_Endpoint( array( 'description' => 'Get details of a user of a site by login.', 'group' => 'users', 'stat' => 'sites:1:user', 'method' => 'GET', 'path' => '/sites/%s/users/login:%s', 'path_labels' => array( '$site' => '(int|string) The site ID or domain.', '$user_id' => '(string) The user\'s login.', ), 'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/login:binarysmash', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_response' => '{ "ID": 18342963, "login": "binarysmash", "email": false, "name": "binarysmash", "URL": "http:\/\/binarysmash.wordpress.com", "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G", "profile_URL": "http:\/\/gravatar.com\/binarysmash", "roles": [ "administrator" ] }', ) ); new WPCOM_JSON_API_Site_User_Endpoint( array( 'description' => 'Update details of a user of a site.', 'group' => 'users', 'stat' => 'sites:1:user', 'method' => 'POST', 'path' => '/sites/%s/users/%d', 'path_labels' => array( '$site' => '(int|string) The site ID or domain.', '$user_id' => '(int) The user\'s ID.', ), 'request_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/23', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'roles' => array( array( 'administrator', ), ), 'first_name' => 'Rocco', 'last_name' => 'Tripaldi', ), ), 'example_response' => '{ "ID": 18342963, "login": "binarysmash", "email": false, "name": "binarysmash", "URL": "http:\/\/binarysmash.wordpress.com", "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G", "profile_URL": "http:\/\/gravatar.com\/binarysmash", "roles": [ "administrator" ] }', ) ); /** * Site user endpoint class. * * /sites/%s/users/%d -> $blog_id, $user_id */ class WPCOM_JSON_API_Site_User_Endpoint extends WPCOM_JSON_API_Endpoint { /** * User format. * * @var array */ public static $user_format = array( 'ID' => '(int) The ID of the user', 'login' => '(string) The login username of the user', 'email' => '(string) The email of the user', 'name' => '(string) The name to display for the user', 'first_name' => '(string) The first name of the user', 'last_name' => '(string) The last name of the user', 'nice_name' => '(string) The nice_name to display for the user', 'URL' => '(string) The primary blog of the user', 'avatar_URL' => '(url) Gravatar image URL', 'profile_URL' => '(url) Gravatar Profile URL', 'site_ID' => '(int) ID of the user\'s primary blog', 'roles' => '(array|string) The role or roles of the user', ); /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $user_id - the user ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0, $user_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can_for_blog( $blog_id, 'list_users' ) ) { return new WP_Error( 'unauthorized', 'User cannot view users for specified site', 403 ); } // Get the user by ID or login $get_by = str_contains( $path, '/users/login:' ) ? 'login' : 'id'; $user = get_user_by( $get_by, $user_id ); if ( ! $user ) { return new WP_Error( 'unknown_user', 'Unknown user', 404 ); } if ( ! is_user_member_of_blog( $user->ID, $blog_id ) ) { return new WP_Error( 'unknown_user_for_site', 'Unknown user for site', 404 ); } if ( 'GET' === $this->api->method ) { return $this->get_user( $user->ID ); } elseif ( 'POST' === $this->api->method ) { if ( ! current_user_can_for_blog( $blog_id, 'promote_users' ) ) { return new WP_Error( 'unauthorized_no_promote_cap', 'User cannot promote users for specified site', 403 ); } return $this->update_user( $user_id, $blog_id ); } else { return new WP_Error( 'bad_request', 'An unsupported request method was used.' ); } } /** * Get the user. * * @param int $user_id - the user ID. * * @return object */ public function get_user( $user_id ) { $the_user = $this->get_author( $user_id, true ); if ( $the_user && ! is_wp_error( $the_user ) ) { $userdata = get_userdata( $user_id ); $the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array(); if ( is_multisite() ) { $the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' ); } } return $the_user; } /** * Updates user data. * * @param int $user_id - the user ID. * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function update_user( $user_id, $blog_id ) { $user = array(); $input = $this->input(); $user['ID'] = $user_id; $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM; if ( get_current_user_id() === (int) $user_id && isset( $input['roles'] ) ) { return new WP_Error( 'unauthorized', 'You cannot change your own role', 403 ); } if ( $is_wpcom && $user_id !== get_current_user_id() && (int) $user_id === wpcom_get_blog_owner( $blog_id ) ) { return new WP_Error( 'unauthorized_edit_owner', 'Current user can not edit blog owner', 403 ); } if ( ! $is_wpcom ) { foreach ( $input as $key => $value ) { if ( ! is_array( $value ) ) { $value = trim( $value ); } $value = wp_unslash( $value ); switch ( $key ) { case 'first_name': case 'last_name': $user[ $key ] = $value; break; case 'display_name': case 'name': $user['display_name'] = $value; break; } } } if ( isset( $input['roles'] ) ) { // For now, we only use the first role in the array. if ( is_array( $input['roles'] ) ) { $user['role'] = $input['roles'][0]; } elseif ( is_string( $input['roles'] ) ) { $user['role'] = $input['roles']; } else { return new WP_Error( 'invalid_input', __( 'The roles property must be a string or an array.', 'jetpack' ), 400 ); } $editable_roles = array_keys( get_editable_roles() ); if ( ! in_array( $user['role'], $editable_roles, true ) ) { return new WP_Error( 'invalid_input', sprintf( /* Translators: placeholder is an invalid role name */ esc_html__( '%s is not a valid role.', 'jetpack' ), $editable_roles ), 400 ); } } $result = wp_update_user( $user ); if ( is_wp_error( $result ) ) { return $result; } return $this->get_user( $user_id ); } } class.wpcom-json-api-list-post-types-endpoint.php 0000644 00000011631 14722054026 0016144 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List post types endpoint. */ new WPCOM_JSON_API_List_Post_Types_Endpoint( array( 'description' => 'Get a list of post types available for a site.', 'group' => 'sites', 'stat' => 'sites:X:post-types', 'method' => 'GET', 'path' => '/sites/%s/post-types', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'api_queryable' => '(bool) If true, only queryable post types are returned', ), 'response_format' => array( 'found' => '(int) The number of post types found', 'post_types' => '(array) A list of available post types', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/33534099/post-types', ) ); /** * List Post types endpoint class. * * /sites/%s/post-types -> $blog_id */ class WPCOM_JSON_API_List_Post_Types_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Post type keys to include. * * @var array */ public static $post_type_keys_to_include = array( 'name' => 'name', 'label' => 'label', 'labels' => 'labels', 'description' => 'description', 'map_meta_cap' => 'map_meta_cap', 'cap' => 'capabilities', 'hierarchical' => 'hierarchical', 'public' => 'public', 'show_ui' => 'show_ui', 'publicly_queryable' => 'publicly_queryable', ); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); /** * Whether API responses should be returned in a custom locale. False * for Jetpack; may be true for WP.com requests. * * @since 3.9.2 */ if ( apply_filters( 'rest_api_localize_response', false ) ) { // API localization occurs after the initial post types have been // registered, so let's get the post type labels translated. if ( 'en' !== get_locale() ) { global $wp_post_types; foreach ( $wp_post_types as $post_type_object ) { foreach ( array_keys( (array) $post_type_object->labels ) as $label_key ) { // Direct use of translate call because this doesn't need to be extracted. // phpcs:ignore WordPress.WP.I18n $post_type_object->labels->$label_key = translate( $post_type_object->labels->$label_key, 'default' ); } } } } } // Get a list of available post types. $post_types = get_post_types(); $formatted_post_type_objects = array(); // Retrieve post type object for each post type. foreach ( $post_types as $post_type ) { // Skip non-queryable if filtering on queryable only. $is_queryable = $this->is_post_type_allowed( $post_type ); if ( ! $is_queryable ) { continue; } $post_type_object = get_post_type_object( $post_type ); $formatted_post_type_object = array(); // Include only the desired keys in the response. foreach ( self::$post_type_keys_to_include as $key => $value ) { $formatted_post_type_object[ $value ] = $post_type_object->{ $key }; } $formatted_post_type_object['api_queryable'] = $is_queryable; $formatted_post_type_object['supports'] = get_all_post_type_supports( $post_type ); if ( $this->post_type_supports_tags( $post_type ) ) { $formatted_post_type_object['supports']['tags'] = true; } $formatted_post_type_objects[] = $formatted_post_type_object; } return array( 'found' => count( $formatted_post_type_objects ), 'post_types' => $formatted_post_type_objects, ); } /** * See if post type supports tags. * * @param string $post_type - the post type. */ public function post_type_supports_tags( $post_type ) { if ( in_array( 'post_tag', get_object_taxonomies( $post_type ), true ) ) { return true; } // the featured content module adds post_tag support // to the post types that are registered for it // however it does so in a way that isn't available // to get_object_taxonomies. $featured_content = get_theme_support( 'featured-content' ); if ( ! $featured_content || empty( $featured_content[0] ) || empty( $featured_content[0]['post_types'] ) ) { return false; } if ( is_array( $featured_content[0]['post_types'] ) ) { return in_array( $post_type, $featured_content[0]['post_types'], true ); } return $post_type === $featured_content[0]['post_types']; } } class.wpcom-json-api-list-post-type-taxonomies-endpoint.php 0000644 00000007715 14722054026 0020155 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List post type taxonomies endpoint. */ new WPCOM_JSON_API_List_Post_Type_Taxonomies_Endpoint( array( 'description' => 'Get a list of taxonomies associated with a post type.', 'group' => 'taxonomy', 'stat' => 'sites:X:post-types:X:taxonomies', 'method' => 'GET', 'path' => '/sites/%s/post-types/%s/taxonomies', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_type' => '(string) Post type', ), 'response_format' => array( 'found' => '(int) The number of taxonomies found', 'taxonomies' => '(array:taxonomy) A list of available taxonomies', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/33534099/post-types/post/taxonomies', ) ); /** * List post type taxonomies endpoint class. * * /sites/%s/post-types/%s/taxonomies -> $blog_id, $post_type */ class WPCOM_JSON_API_List_Post_Type_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Included taxonomy keys. * * @var array */ public static $taxonomy_keys_to_include = array( 'name' => 'name', 'label' => 'label', 'labels' => 'labels', 'description' => 'description', 'hierarchical' => 'hierarchical', 'public' => 'public', 'cap' => 'capabilities', ); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. * @param string $post_type - the post type. */ public function callback( $path = '', $blog_id = 0, $post_type = 'post' ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } $this->localize_initial_taxonomies( $post_type ); $post_type_object = get_post_type_object( $post_type ); if ( ! $post_type_object || ( ! $post_type_object->publicly_queryable && ( ! current_user_can( $post_type_object->cap->edit_posts ) ) ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } // Get a list of available taxonomies. $taxonomy_objects = get_object_taxonomies( $post_type, 'objects' ); // Construct array of formatted objects. $formatted_taxonomy_objects = array(); foreach ( $taxonomy_objects as $taxonomy_object ) { // Omit private taxonomies unless user has assign capability. if ( ! $taxonomy_object->public && ! current_user_can( $taxonomy_object->cap->assign_terms ) ) { continue; } // Include only the desired keys in the response. $formatted_taxonomy_object = array(); foreach ( self::$taxonomy_keys_to_include as $key => $value ) { $formatted_taxonomy_object[ $value ] = $taxonomy_object->{ $key }; } $formatted_taxonomy_objects[] = $formatted_taxonomy_object; } return array( 'found' => count( $formatted_taxonomy_objects ), 'taxonomies' => $formatted_taxonomy_objects, ); } /** * Handle localizing initial taxonomies. * * @param string $post_type - the post type. */ protected function localize_initial_taxonomies( $post_type ) { /** This filter is documented in jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php */ if ( ! apply_filters( 'rest_api_localize_response', false ) ) { return; } // Since recreating initial taxonomies will restore the default post // types to which they are associated, save post type's taxonomies in // case it was customized via `register_taxonomy_for_object_type`. $post_type_taxonomies = get_object_taxonomies( $post_type ); // API localization occurs after the initial taxonomies have been // registered, so re-register if localizing response. create_initial_taxonomies(); // Restore registered taxonomies for post type. foreach ( $post_type_taxonomies as $taxonomy ) { register_taxonomy_for_object_type( $taxonomy, $post_type ); } } } class.wpcom-json-api-update-comment-endpoint.php 0000644 00000035524 14722054026 0015775 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Manage comments via the WordPress.com REST API. * * Endpoints; * Create a comment on a post: /sites/%s/posts/%d/replies/new * Create a comment as a reply to another comment: /sites/%s/comments/%d/replies/new * Edit a comment: /sites/%s/comments/%d * Delete a comment: /sites/%s/comments/%d/delete */ use Automattic\Jetpack\Status; new WPCOM_JSON_API_Update_Comment_Endpoint( array( 'description' => 'Create a comment on a post.', 'group' => 'comments', 'stat' => 'posts:1:replies:new', 'method' => 'POST', 'path' => '/sites/%s/posts/%d/replies/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'request_format' => array( // explicitly document all input. 'content' => '(HTML) The comment text.', // @todo Should we open this up to unauthenticated requests too? // 'author' => '(author object) The author of the comment.', ), 'pass_wpcom_user_details' => true, 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/843/replies/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'content' => 'Your reply is very interesting. This is a reply.', ), ), ) ); new WPCOM_JSON_API_Update_Comment_Endpoint( array( 'description' => 'Create a comment as a reply to another comment.', 'group' => 'comments', 'stat' => 'comments:1:replies:new', 'method' => 'POST', 'path' => '/sites/%s/comments/%d/replies/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$comment_ID' => '(int) The comment ID', ), 'request_format' => array( 'content' => '(HTML) The comment text.', // @todo Should we open this up to unauthenticated requests too? // 'author' => '(author object) The author of the comment.', ), 'pass_wpcom_user_details' => true, 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29/replies/new', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'content' => 'This reply is very interesting. This is editing a comment reply via the API.', ), ), ) ); new WPCOM_JSON_API_Update_Comment_Endpoint( array( 'description' => 'Edit a comment.', 'group' => 'comments', 'stat' => 'comments:1:POST', 'method' => 'POST', 'path' => '/sites/%s/comments/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$comment_ID' => '(int) The comment ID', ), 'request_format' => array( 'author' => "(string) The comment author's name.", 'author_email' => "(string) The comment author's email.", 'author_url' => "(string) The comment author's URL.", 'content' => '(HTML) The comment text.', 'date' => "(ISO 8601 datetime) The comment's creation time.", 'status' => array( 'approved' => 'Approve the comment.', 'unapproved' => 'Remove the comment from public view and send it to the moderation queue.', 'spam' => 'Mark the comment as spam.', 'unspam' => 'Unmark the comment as spam. Will attempt to set it to the previous status.', 'trash' => 'Send a comment to the trash if trashing is enabled (see constant: EMPTY_TRASH_DAYS).', 'untrash' => 'Untrash a comment. Only works when the comment is in the trash.', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'content' => 'This reply is now edited via the API.', 'status' => 'approved', ), ), ) ); new WPCOM_JSON_API_Update_Comment_Endpoint( array( 'description' => 'Delete a comment.', 'group' => 'comments', 'stat' => 'comments:1:delete', 'method' => 'POST', 'path' => '/sites/%s/comments/%d/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$comment_ID' => '(int) The comment ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/$comment_ID/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Update comments endpoint class. */ class WPCOM_JSON_API_Update_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint { /** * WPCOM_JSON_API_Update_Comment_Endpoint constructor. * * @param array $args - Args. */ public function __construct( $args ) { parent::__construct( $args ); if ( $this->api->ends_with( $this->path, '/delete' ) ) { $this->comment_object_format['status']['deleted'] = 'The comment has been deleted permanently.'; } } /** * Update comment API callback. * * /sites/%s/posts/%d/replies/new -> $blog_id, $post_id * /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id * /sites/%s/comments/%d -> $blog_id, $comment_id * /sites/%s/comments/%d/delete -> $blog_id, $comment_id * * @param string $path API path. * @param int $blog_id The blog ID. * @param int $object_id The object ID. * * @return bool|WP_Error|array */ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) { if ( $this->api->ends_with( $path, '/new' ) ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false ); } else { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); } if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_comment( $path, $blog_id, $object_id ); } elseif ( $this->api->ends_with( $path, '/new' ) ) { if ( str_contains( $path, '/posts/' ) ) { return $this->new_comment( $path, $blog_id, $object_id, 0 ); } else { return $this->new_comment( $path, $blog_id, 0, $object_id ); } } return $this->update_comment( $path, $blog_id, $object_id ); } /** * Add a new comment to a post or as a reply to another comment. * * /sites/%s/posts/%d/replies/new -> $blog_id, $post_id * /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id * * @param string $path API path. * @param int $blog_id The blog ID. * @param int $post_id The post ID. * @param int $comment_parent_id The comment parent ID. * * @return bool|WP_Error|array */ public function new_comment( $path, $blog_id, $post_id, $comment_parent_id ) { $comment_parent = null; if ( ! $post_id ) { $comment_parent = get_comment( $comment_parent_id ); if ( ! $comment_parent_id || ! $comment_parent || is_wp_error( $comment_parent ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } $post_id = $comment_parent->comment_post_ID; } $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ( new Status() )->is_private_site() && /** * Filter allowing non-registered users on the site to comment. * * @module json-api * * @since 3.4.0 * * @param bool is_user_member_of_blog() Is the user member of the site. */ ! apply_filters( 'wpcom_json_api_user_is_member_of_blog', is_user_member_of_blog() ) && ! is_super_admin() ) { return new WP_Error( 'unauthorized', 'User cannot create comments', 403 ); } if ( ! comments_open( $post->ID ) && ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'Comments on this post are closed', 403 ); } $can_view = $this->user_can_view_post( $post->ID ); if ( ! $can_view || is_wp_error( $can_view ) ) { return $can_view; } $post_status = get_post_status_object( get_post_status( $post ) ); if ( ! $post_status->public && ! $post_status->private ) { return new WP_Error( 'unauthorized', 'Comments on drafts are not allowed', 403 ); } $args = $this->query_args(); $input = $this->input(); if ( ! is_array( $input ) || ! $input || ! strlen( $input['content'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $user = wp_get_current_user(); if ( ! $user || is_wp_error( $user ) || ! $user->ID ) { $auth_required = false; if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $auth_required = true; } elseif ( isset( $this->api->token_details['user'] ) ) { $user = (object) $this->api->token_details['user']; foreach ( array( 'display_name', 'user_email', 'user_url' ) as $user_datum ) { if ( ! isset( $user->$user_datum ) ) { $auth_required = true; } } if ( ! isset( $user->ID ) ) { $user->ID = 0; } $author = get_user_by( 'id', (int) $user->ID ); // If we have a user with an external ID saved, we can use it. if ( ! $auth_required && $user->ID && $author ) { $user = $author; } } else { $auth_required = true; } if ( $auth_required ) { return new WP_Error( 'authorization_required', 'An active access token must be used to comment.', 403 ); } } $insert = array( 'comment_post_ID' => $post->ID, 'user_ID' => $user->ID, 'comment_author' => $user->display_name, 'comment_author_email' => $user->user_email, 'comment_author_url' => $user->user_url, 'comment_content' => $input['content'], 'comment_parent' => $comment_parent_id, 'comment_type' => 'comment', ); if ( $comment_parent_id ) { if ( '0' === $comment_parent->comment_approved && current_user_can( 'edit_comment', $comment_parent->comment_ID ) ) { wp_set_comment_status( $comment_parent->comment_ID, 'approve' ); } } $this->api->trap_wp_die( 'comment_failure' ); $comment_id = wp_new_comment( add_magic_quotes( $insert ) ); $this->api->trap_wp_die( null ); $return = $this->get_comment( $comment_id, $args['context'] ); if ( ! $return ) { return new WP_Error( 400, __( 'Comment cache problem?', 'jetpack' ) ); } if ( is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'comments' ); return $return; } /** * Update a comment. * * /sites/%s/comments/%d -> $blog_id, $comment_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $comment_id Comment ID. * * @return bool|WP_Error|array */ public function update_comment( $path, $blog_id, $comment_id ) { $comment = get_comment( $comment_id ); if ( ! $comment || is_wp_error( $comment ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 ); } $args = $this->query_args(); $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $update = array(); foreach ( $input as $key => $value ) { $update[ "comment_$key" ] = $value; } $comment_status = wp_get_comment_status( $comment->comment_ID ); if ( isset( $update['comment_status'] ) ) { switch ( $update['comment_status'] ) { case 'approved': if ( 'approve' !== $comment_status ) { wp_set_comment_status( $comment->comment_ID, 'approve' ); } break; case 'unapproved': if ( 'hold' !== $comment_status ) { wp_set_comment_status( $comment->comment_ID, 'hold' ); } break; case 'spam': if ( 'spam' !== $comment_status ) { wp_spam_comment( $comment->comment_ID ); } break; case 'unspam': if ( 'spam' === $comment_status ) { wp_unspam_comment( $comment->comment_ID ); } break; case 'trash': if ( ! EMPTY_TRASH_DAYS ) { return new WP_Error( 'trash_disabled', 'Cannot trash comment', 403 ); } if ( 'trash' !== $comment_status ) { wp_trash_comment( $comment_id ); } break; case 'untrash': if ( 'trash' === $comment_status ) { wp_untrash_comment( $comment->comment_ID ); } break; default: $update['comment_approved'] = 1; break; } unset( $update['comment_status'] ); } if ( ! empty( $update ) ) { $update['comment_ID'] = $comment->comment_ID; wp_update_comment( add_magic_quotes( $update ) ); } $return = $this->get_comment( $comment->comment_ID, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'comments' ); return $return; } /** * Delete a comment. * * /sites/%s/comments/%d/delete -> $blog_id, $comment_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $comment_id Comment ID. * * @return bool|WP_Error|array */ public function delete_comment( $path, $blog_id, $comment_id ) { $comment = get_comment( $comment_id ); if ( ! $comment || is_wp_error( $comment ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { // [sic] There is no delete_comment cap return new WP_Error( 'unauthorized', 'User cannot delete comment', 403 ); } $args = $this->query_args(); $return = $this->get_comment( $comment->comment_ID, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'comments' ); wp_delete_comment( $comment->comment_ID ); $status = wp_get_comment_status( $comment->comment_ID ); if ( false === $status ) { $return['status'] = 'deleted'; return $return; } return $this->get_comment( $comment->comment_ID, $args['context'] ); } } class.wpcom-json-api-list-media-v1-1-endpoint.php 0000644 00000024002 14722054026 0015552 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List Media v1_1 endpoint. */ new WPCOM_JSON_API_List_Media_v1_1_Endpoint( array( 'description' => 'Get a list of items in the media library.', 'group' => 'media', 'stat' => 'media', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'GET', 'path' => '/sites/%s/media/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=20) The number of media items to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.', 'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.', 'order' => array( 'DESC' => 'Return files in descending order. For dates, that means newest to oldest.', 'ASC' => 'Return files in ascending order. For dates, that means oldest to newest.', ), 'order_by' => array( 'date' => 'Order by the uploaded time of each file.', 'title' => 'Order lexicographically by file titles.', 'ID' => 'Order by media ID.', ), 'search' => '(string) Search query.', 'post_ID' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.', 'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).", 'after' => '(ISO 8601 datetime) Return media items uploaded after the specified datetime.', 'before' => '(ISO 8601 datetime) Return media items uploaded before the specified datetime.', ), 'response_format' => array( 'media' => '(array) Array of media objects', 'found' => '(int) The number of total results found', 'meta' => '(object) Meta data', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List media v1_1 endpoint class. */ class WPCOM_JSON_API_List_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore /** * Date range. * * @var array */ public $date_range = array(); /** * The page handle. * * @var array */ public $page_handle = array(); /** * Performed query * * @var array */ public $performed_query = array(); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // upload_files can probably be used for other endpoints but we want contributors to be able to use media too. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $args = $this->query_args(); $is_eligible_for_page_handle = true; if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $this->load_theme_functions(); } if ( isset( $args['before'] ) ) { $this->date_range['before'] = $args['before']; } if ( isset( $args['after'] ) ) { $this->date_range['after'] = $args['after']; } $query = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => isset( $args['post_ID'] ) ? $args['post_ID'] : null, 'posts_per_page' => $args['number'], 'post_mime_type' => isset( $args['mime_type'] ) ? $args['mime_type'] : null, 'order' => isset( $args['order'] ) ? $args['order'] : 'DESC', 'orderby' => isset( $args['order_by'] ) ? $args['order_by'] : 'date', 's' => isset( $args['search'] ) ? $args['search'] : null, 'meta_query' => array( array( 'key' => 'videopress_poster_image', 'compare' => 'NOT EXISTS', ), ), ); if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $query['paged'] = $args['page']; if ( 1 !== $query['paged'] ) { $is_eligible_for_page_handle = false; } } else { if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $query['offset'] = $args['offset']; if ( 0 !== $query['offset'] ) { $is_eligible_for_page_handle = false; } } if ( isset( $args['page_handle'] ) ) { $page_handle = wp_parse_args( $args['page_handle'] ); if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) { // we have a valid looking page handle. $this->page_handle = $page_handle; add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } } if ( $this->date_range ) { add_filter( 'posts_where', array( $this, 'handle_date_range' ) ); } $this->performed_query = $query; add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); $media = new WP_Query( $query ); remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); if ( $this->date_range ) { remove_filter( 'posts_where', array( $this, 'handle_date_range' ) ); $this->date_range = array(); } if ( $this->page_handle ) { remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } $response = array(); foreach ( $media->posts as $item ) { $response[] = $this->get_media_item_v1_1( $item->ID ); } $return = array( 'found' => (int) $media->found_posts, 'media' => $response, ); if ( $is_eligible_for_page_handle && $return['media'] ) { $last_post = end( $return['media'] ); reset( $return['media'] ); if ( ( $return['found'] > count( $return['media'] ) ) && $last_post ) { $return['meta'] = array(); $return['meta']['next_page'] = $this->build_page_handle( $last_post, $query ); } } return $return; } /** * Build the page handle. * * @param object $post - the post object. * @param array $query - the query. */ public function build_page_handle( $post, $query ) { $column = $query['orderby']; if ( ! $column ) { $column = 'date'; } return build_query( array( 'value' => rawurlencode( $post->$column ), 'id' => $post->ID, ) ); } /** * Handle figuring out the page handler is. * * @param string $where - sql where clause. */ public function handle_where_for_page_handle( $where ) { global $wpdb; $column = $this->performed_query['orderby']; if ( ! $column ) { $column = 'date'; } $order = $this->performed_query['order']; if ( ! $order ) { $order = 'DESC'; } if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ), true ) ) { return $where; } if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) { return $where; } $db_column = ''; $db_value = ''; switch ( $column ) { case 'ID': $db_column = 'ID'; $db_value = '%d'; break; case 'title': $db_column = 'post_title'; $db_value = '%s'; break; case 'date': $db_column = 'post_date'; $db_value = 'CAST( %s as DATETIME )'; break; case 'modified': $db_column = 'post_modified'; $db_value = 'CAST( %s as DATETIME )'; break; case 'comment_count': $db_column = 'comment_count'; $db_value = '%d'; break; } if ( 'DESC' === $order ) { $db_order = '<'; } else { $db_order = '>'; } // Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item // but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items // beyond the passed item. $where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( 'ID' !== $db_column ) { $where .= $wpdb->prepare( "OR ( `$wpdb->posts`.`$db_column` = $db_value AND `$wpdb->posts`.ID $db_order %d )", $this->page_handle['value'], $this->page_handle['id'] ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } $where .= ' )'; return $where; } /** * Handle date range. * * @param string $where - sql where clause. */ public function handle_date_range( $where ) { global $wpdb; switch ( count( $this->date_range ) ) { case 2: $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ", $this->date_range['after'], $this->date_range['before'] ); break; case 1: if ( isset( $this->date_range['before'] ) ) { $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date <= CAST( %s AS DATETIME ) ", $this->date_range['before'] ); } else { $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date >= CAST( %s AS DATETIME ) ", $this->date_range['after'] ); } break; } return $where; } /** * Handle how page handle is ordered by. * * @param string $orderby - how we want to order things by. */ public function handle_orderby_for_page_handle( $orderby ) { global $wpdb; if ( 'ID' === $this->performed_query['orderby'] ) { // bail if we're already ordering by ID. return $orderby; } if ( $orderby ) { $orderby .= ' ,'; } $order = $this->performed_query['order']; if ( ! $order ) { $order = 'DESC'; } $orderby .= " `$wpdb->posts`.ID $order"; return $orderby; } } class.wpcom-json-api-update-site-logo-endpoint.php 0000644 00000006246 14722054026 0016234 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Set site logo settings API. * * Endpoints: * Set site logo settings: /sites/%s/logo * Delete site logo settings: /sites/%s/logo/delete */ new WPCOM_JSON_API_Update_Site_Logo_Endpoint( array( 'description' => 'Set site logo settings', 'group' => '__do_not_document', 'stat' => 'sites:1:logo', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/logo', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'request_format' => array( 'id' => '(int) The ID of the logo post', 'url' => '(string) The URL of the logo post (deprecated)', ), 'response_format' => array( 'id' => '(int) The ID of the logo post', 'url' => '(string) The URL of the logo post', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/logo', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'id' => 12345, ), ), 'example_response' => ' { "id": 12345, "url": "https:\/\/s.w.org\/about\/images\/logos\/codeispoetry-rgb.png" }', ) ); new WPCOM_JSON_API_Update_Site_Logo_Endpoint( array( 'description' => 'Delete site logo settings', 'group' => '__do_not_document', 'stat' => 'sites:1:logo:delete', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/logo/delete', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/logo/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); /** * Set site logo settings API class. */ class WPCOM_JSON_API_Update_Site_Logo_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Set site logo settings API callback. * * @param string $path API path. * @param int $site_id Blog ID. */ public function callback( $path = '', $site_id = 0 ) { // Switch to the given blog. $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( 'unauthorized', 'User is not authorized to access logo settings', 403 ); } if ( strpos( $path, '/delete' ) ) { delete_option( 'site_logo' ); return array(); } $args = $this->input(); $logo_settings = $this->get_current_settings(); if ( empty( $args ) || ! is_array( $args ) ) { return $logo_settings; } if ( isset( $args['id'] ) ) { update_option( 'site_logo', (int) $args['id'] ); } return $this->get_current_settings(); } /** * Get current logo settings. */ public function get_current_settings() { $logo_id = get_option( 'site_logo' ); if ( ! $logo_id ) { return array(); } return array( 'id' => $logo_id, 'url' => wp_get_attachment_url( $logo_id ), ); } } class.wpcom-json-api-post-endpoint.php 0000644 00000054412 14722054026 0014035 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Post Endpoint class. */ abstract class WPCOM_JSON_API_Post_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Post object format. * * @var array */ public $post_object_format = array( // explicitly document and cast all output 'ID' => '(int) The post ID.', 'site_ID' => '(int) The site ID.', 'author' => '(object>author) The author of the post.', 'date' => "(ISO 8601 datetime) The post's creation time.", 'modified' => "(ISO 8601 datetime) The post's most recent update time.", 'title' => '(HTML) <code>context</code> dependent.', 'URL' => '(URL) The full permalink URL to the post.', 'short_URL' => '(URL) The wp.me short URL.', 'content' => '(HTML) <code>context</code> dependent.', 'excerpt' => '(HTML) <code>context</code> dependent.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'guid' => '(string) The GUID for the post.', 'status' => array( 'publish' => 'The post is published.', 'draft' => 'The post is saved as a draft.', 'pending' => 'The post is pending editorial approval.', 'private' => 'The post is published privately', 'future' => 'The post is scheduled for future publishing.', 'trash' => 'The post is in the trash.', 'auto-draft' => 'The post is a placeholder for a new post.', ), 'sticky' => '(bool) Is the post sticky?', 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(object>post_reference|false) A reference to the post's parent, if it has one.", 'type' => "(string) The post's post_type. Post types besides post, page and revision need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'comments_open' => '(bool) Is the post open for comments?', 'pings_open' => '(bool) Is the post open for pingbacks, trackbacks?', 'likes_enabled' => '(bool) Is the post open to likes?', 'sharing_enabled' => '(bool) Should sharing buttons show on this post?', 'comment_count' => '(int) The number of comments for this post.', 'like_count' => '(int) The number of likes for this post.', 'i_like' => '(bool) Does the current user like this post?', 'is_reblogged' => '(bool) Did the current user reblog this post?', 'is_following' => '(bool) Is the current user following this blog?', 'global_ID' => '(string) A unique WordPress.com-wide representation of a post.', 'featured_image' => '(URL) The URL to the featured image for this post if it has one.', 'post_thumbnail' => '(object>attachment) The attachment object for the featured image if it has one.', 'format' => array(), // see constructor 'geo' => '(object>geo|false)', 'menu_order' => '(int) (Pages Only) The order pages should appear in.', 'publicize_URLs' => '(array:URL) Array of Facebook URLs published by this post.', 'tags' => '(object:tag) Hash of tags (keyed by tag name) applied to the post.', 'categories' => '(object:category) Hash of categories (keyed by category name) applied to the post.', 'attachments' => '(object:attachment) Hash of post attachments (keyed by attachment ID).', 'metadata' => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'meta' => '(object) API result meta data', 'current_user_can' => '(object) List of permissions. Note, deprecated in favor of `capabilities`', 'capabilities' => '(object) List of post-specific permissions for the user; publish_post, edit_post, delete_post', ); /** * Constructor function. * * @param string|array|object $args — Args. */ public function __construct( $args ) { if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) { $this->post_object_format['format'] = get_post_format_strings(); } if ( ! $this->response_format ) { $this->response_format =& $this->post_object_format; } parent::__construct( $args ); } /** * Filter to replace the password form with a simple message that the post is protected. * * @return string */ public function the_password_form() { return __( 'This post is password protected.', 'jetpack' ); } /** * Get a post by a specified field and value * * @param string $field - the field. * @param string $field_value - the field value. * @param string $context Post use context (e.g. 'display'). * @return array|bool|WP_Error Post **/ public function get_post_by( $field, $field_value, $context = 'display' ) { global $blog_id; /** This filter is documented in class.json-api-endpoints.php */ $is_jetpack = true === apply_filters( 'is_jetpack_site', false, $blog_id ); if ( defined( 'GEO_LOCATION__CLASS' ) && class_exists( GEO_LOCATION__CLASS ) ) { $geo = call_user_func( array( GEO_LOCATION__CLASS, 'init' ) ); } else { $geo = false; } if ( 'display' === $context ) { $args = $this->query_args(); if ( isset( $args['content_width'] ) && $args['content_width'] ) { $GLOBALS['content_width'] = (int) $args['content_width']; } } switch ( $field ) { case 'name': $post_id = $this->get_post_id_by_name( $field_value ); if ( is_wp_error( $post_id ) ) { return $post_id; } break; default: $post_id = (int) $field_value; break; } $post = get_post( $post_id, OBJECT, $context ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! $this->is_post_type_allowed( $post->post_type ) && ( ! function_exists( 'is_post_freshly_pressed' ) || ! is_post_freshly_pressed( $post->ID ) ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } // Permissions $capabilities = $this->get_current_user_capabilities( $post ); switch ( $context ) { case 'edit': if ( ! $capabilities['edit_post'] ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } break; case 'display': break; default: return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 ); } $can_view = $this->user_can_view_post( $post->ID ); if ( ! $can_view || is_wp_error( $can_view ) ) { return $can_view; } $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited if ( 'display' === $context ) { setup_postdata( $post ); } $response = array(); $fields = null; if ( 'display' === $context && ! empty( $this->api->query['fields'] ) ) { $fields = array_fill_keys( array_map( 'trim', explode( ',', $this->api->query['fields'] ) ), true ); } foreach ( array_keys( $this->post_object_format ) as $key ) { if ( $fields !== null && ! isset( $fields[ $key ] ) ) { continue; } switch ( $key ) { case 'ID': // explicitly cast all output $response[ $key ] = (int) $post->ID; break; case 'site_ID': $response[ $key ] = (int) $this->api->get_blog_id_for_output(); break; case 'author': $response[ $key ] = (object) $this->get_author( $post, 'edit' === $context && $capabilities['edit_post'] ); break; case 'date': $response[ $key ] = (string) $this->format_date( $post->post_date_gmt, $post->post_date ); break; case 'modified': $response[ $key ] = (string) $this->format_date( $post->post_modified_gmt, $post->post_modified ); break; case 'title': if ( 'display' === $context ) { $response[ $key ] = (string) get_the_title( $post->ID ); } else { $response[ $key ] = (string) htmlspecialchars_decode( $post->post_title, ENT_QUOTES ); } break; case 'URL': if ( 'revision' === $post->post_type ) { $response[ $key ] = (string) esc_url_raw( get_permalink( $post->post_parent ) ); } else { $response[ $key ] = (string) esc_url_raw( get_permalink( $post->ID ) ); } break; case 'short_URL': $response[ $key ] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) ); break; case 'content': if ( 'display' === $context ) { add_filter( 'the_password_form', array( $this, 'the_password_form' ) ); $response[ $key ] = (string) $this->get_the_post_content_for_display(); remove_filter( 'the_password_form', array( $this, 'the_password_form' ) ); } else { $response[ $key ] = (string) $post->post_content; } break; case 'excerpt': if ( 'display' === $context ) { add_filter( 'the_password_form', array( $this, 'the_password_form' ) ); ob_start(); the_excerpt(); $response[ $key ] = (string) ob_get_clean(); remove_filter( 'the_password_form', array( $this, 'the_password_form' ) ); } else { $response[ $key ] = htmlspecialchars_decode( (string) $post->post_excerpt, ENT_QUOTES ); } break; case 'status': $response[ $key ] = (string) get_post_status( $post->ID ); break; case 'sticky': $response[ $key ] = (bool) is_sticky( $post->ID ); break; case 'slug': $response[ $key ] = (string) $post->post_name; break; case 'guid': $response[ $key ] = (string) $post->guid; break; case 'password': $response[ $key ] = (string) $post->post_password; if ( 'edit' === $context ) { $response[ $key ] = htmlspecialchars_decode( (string) $response[ $key ], ENT_QUOTES ); } break; /** (object|false) */ case 'parent': if ( $post->post_parent ) { $parent = get_post( $post->post_parent ); if ( 'display' === $context ) { $parent_title = (string) get_the_title( $parent->ID ); } else { $parent_title = (string) htmlspecialchars_decode( $post->post_title, ENT_QUOTES ); } $response[ $key ] = (object) array( 'ID' => (int) $parent->ID, 'type' => (string) $parent->post_type, 'link' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $parent->ID ), 'title' => $parent_title, ); } else { $response[ $key ] = false; } break; case 'type': $response[ $key ] = (string) $post->post_type; break; case 'comments_open': $response[ $key ] = (bool) comments_open( $post->ID ); break; case 'pings_open': $response[ $key ] = (bool) pings_open( $post->ID ); break; case 'likes_enabled': /** This filter is documented in modules/likes.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); $post_likes_switched = get_post_meta( $post->ID, 'switch_like_status', true ); $post_likes_enabled = $post_likes_switched || ( $sitewide_likes_enabled && $post_likes_switched !== '0' ); $response[ $key ] = (bool) $post_likes_enabled; break; case 'sharing_enabled': $show = true; /** This filter is documented in modules/sharedaddy/sharing-service.php */ $show = apply_filters( 'sharing_show', $show, $post ); $switched_status = get_post_meta( $post->ID, 'sharing_disabled', false ); if ( ! empty( $switched_status ) ) { $show = false; } $response[ $key ] = (bool) $show; break; case 'comment_count': $response[ $key ] = (int) $post->comment_count; break; case 'like_count': $response[ $key ] = (int) $this->api->post_like_count( $blog_id, $post->ID ); break; case 'i_like': $response[ $key ] = (bool) $this->api->is_liked( $blog_id, $post->ID ); break; case 'is_reblogged': $response[ $key ] = (bool) $this->api->is_reblogged( $blog_id, $post->ID ); break; case 'is_following': $response[ $key ] = (bool) $this->api->is_following( $blog_id ); break; case 'global_ID': $response[ $key ] = (string) $this->api->add_global_ID( $blog_id, $post->ID ); break; case 'featured_image': if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { $response[ $key ] = get_post_meta( $post->ID, '_jetpack_featured_image', true ); } else { $image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' ); if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) { $response[ $key ] = (string) $image_attributes[0]; } else { $response[ $key ] = ''; } } break; case 'post_thumbnail': $response[ $key ] = null; $thumb_id = get_post_thumbnail_id( $post->ID ); if ( ! empty( $thumb_id ) ) { $attachment = get_post( $thumb_id ); if ( ! empty( $attachment ) ) { $featured_image_object = $this->get_attachment( $attachment ); } if ( ! empty( $featured_image_object ) ) { $response[ $key ] = (object) $featured_image_object; } } break; case 'format': $response[ $key ] = (string) get_post_format( $post->ID ); if ( ! $response[ $key ] ) { $response[ $key ] = 'standard'; } break; /** (object|false) */ case 'geo': if ( ! $geo ) { $response[ $key ] = false; } else { $geo_data = $geo->get_geo( 'post', $post->ID ); $response[ $key ] = false; if ( $geo_data ) { $geo_data = array_intersect_key( $geo_data, array( 'latitude' => true, 'longitude' => true, 'address' => true, 'public' => true, ) ); if ( $geo_data ) { $response[ $key ] = (object) array( 'latitude' => isset( $geo_data['latitude'] ) ? (float) $geo_data['latitude'] : 0, 'longitude' => isset( $geo_data['longitude'] ) ? (float) $geo_data['longitude'] : 0, 'address' => isset( $geo_data['address'] ) ? (string) $geo_data['address'] : '', ); } else { $response[ $key ] = false; } // Private if ( ! isset( $geo_data['public'] ) || ! $geo_data['public'] ) { if ( 'edit' !== $context || ! $capabilities['edit_post'] ) { // user can't access $response[ $key ] = false; } } } } break; case 'menu_order': $response[ $key ] = (int) $post->menu_order; break; case 'publicize_URLs': $publicize_urls = array(); $publicize = get_post_meta( $post->ID, 'publicize_results', true ); if ( $publicize ) { foreach ( $publicize as $service => $data ) { switch ( $service ) { // @todo Explore removing once Twitter has been removed from Publicize. case 'twitter': foreach ( $data as $datum ) { $publicize_urls[] = esc_url_raw( "https://twitter.com/{$datum['user_id']}/status/{$datum['post_id']}" ); } break; case 'fb': foreach ( $data as $datum ) { $publicize_urls[] = esc_url_raw( "https://www.facebook.com/permalink.php?story_fbid={$datum['post_id']}&id={$datum['user_id']}" ); } break; } } } $response[ $key ] = (array) $publicize_urls; break; case 'tags': $response[ $key ] = array(); $terms = wp_get_post_tags( $post->ID ); foreach ( $terms as $term ) { if ( ! empty( $term->name ) ) { $response[ $key ][ $term->name ] = $this->format_taxonomy( $term, 'post_tag', 'display' ); } } $response[ $key ] = (object) $response[ $key ]; break; case 'categories': $response[ $key ] = array(); $terms = wp_get_object_terms( $post->ID, 'category', array( 'fields' => 'all' ) ); foreach ( $terms as $term ) { if ( ! empty( $term->name ) ) { $response[ $key ][ $term->name ] = $this->format_taxonomy( $term, 'category', 'display' ); } } $response[ $key ] = (object) $response[ $key ]; break; case 'attachments': $response[ $key ] = array(); $_attachments = get_posts( array( 'post_parent' => $post->ID, 'post_status' => 'inherit', 'post_type' => 'attachment', 'posts_per_page' => 100, ) ); foreach ( $_attachments as $attachment ) { $response[ $key ][ $attachment->ID ] = $this->get_attachment( $attachment ); } $response[ $key ] = (object) $response[ $key ]; break; /** (array|false) */ case 'metadata': $metadata = array(); foreach ( (array) has_meta( $post_id ) as $meta ) { // Don't expose protected fields. $show = false; if ( WPCOM_JSON_API_Metadata::is_public( $meta['meta_key'] ) ) { $show = true; } if ( current_user_can( 'edit_post_meta', $post_id, $meta['meta_key'] ) ) { $show = true; } if ( in_array( $meta['meta_key'], Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) && ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { $show = false; } if ( ! $show ) { continue; } $metadata[] = array( 'id' => $meta['meta_id'], 'key' => $meta['meta_key'], 'value' => maybe_unserialize( $meta['meta_value'] ), ); } if ( ! empty( $metadata ) ) { $response[ $key ] = $metadata; } else { $response[ $key ] = false; } break; case 'meta': $response[ $key ] = (object) array( 'links' => (object) array( 'self' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ), 'help' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'help' ), 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ), 'replies' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'replies/' ), 'likes' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'likes/' ), ), ); break; case 'current_user_can': $response[ $key ] = $capabilities; break; case 'capabilities': $response[ $key ] = $capabilities; break; } } unset( $GLOBALS['post'] ); return $response; } /** * * Get the post content for display. * * No Blog ID parameter. No Post ID parameter. Depends on globals. * Expects setup_postdata() to already have been run. * * @return string|false */ public function get_the_post_content_for_display() { global $pages, $page; $old_pages = $pages; $old_page = $page; $content = implode( "\n\n", $pages ); $content = preg_replace( '/<!--more(.*?)?-->/', '', $content ); $pages = array( $content ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $page = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ob_start(); the_content(); $return = ob_get_clean(); $pages = $old_pages; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $page = $old_page; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited return $return; } /** * Gets the blog post. * * @param int $blog_id - the blog ID. * @param int $post_id - the post ID. * @param string $context - the context. * @return array|bool|WP_Error Post */ public function get_blog_post( $blog_id, $post_id, $context = 'display' ) { $blog_id = $this->api->get_blog_id( $blog_id ); if ( ! $blog_id || is_wp_error( $blog_id ) ) { return $blog_id; } switch_to_blog( $blog_id ); $post = $this->get_post_by( 'ID', $post_id, $context ); restore_current_blog(); return $post; } /** * Supporting featured media in post endpoints. Currently on for wpcom blogs * since it's calling WPCOM_JSON_API_Read_Endpoint methods which presently * rely on wpcom specific functionality. * * @param WP_Post $post - the WP Post object. * @return object list of featured media */ public static function find_featured_media( &$post ) { if ( class_exists( 'WPCOM_JSON_API_Read_Endpoint' ) ) { return WPCOM_JSON_API_Read_Endpoint::find_featured_worthy_media( (array) $post ); } else { return (object) array(); } } /** * Returns attachment object. * * @param object $attachment attachment row. * * @return object */ public function get_attachment( $attachment ) { $metadata = wp_get_attachment_metadata( $attachment->ID ); $result = array( 'ID' => (int) $attachment->ID, 'URL' => (string) wp_get_attachment_url( $attachment->ID ), 'guid' => (string) $attachment->guid, 'mime_type' => (string) $attachment->post_mime_type, 'width' => (int) isset( $metadata['width'] ) ? $metadata['width'] : 0, 'height' => (int) isset( $metadata['height'] ) ? $metadata['height'] : 0, ); if ( isset( $metadata['duration'] ) ) { $result['duration'] = (int) $metadata['duration']; } return (object) apply_filters( 'get_attachment', $result ); } /** * Get post-specific user capabilities * * @param WP_Post $post - the WP_Post object. * * @return array - array of post-level permissions; 'publish_post', 'delete_post', 'edit_post' */ public function get_current_user_capabilities( $post ) { return array( 'publish_post' => current_user_can( 'publish_post', $post->ID ), 'delete_post' => current_user_can( 'delete_post', $post->ID ), 'edit_post' => current_user_can( 'edit_post', $post->ID ), ); } /** * Get post ID by name * * Attempts to match name on post title and page path * * @param string $name - the name of the post. * * @return int|object Post ID on success, WP_Error object on failure **/ protected function get_post_id_by_name( $name ) { $name = sanitize_title( $name ); if ( ! $name ) { return new WP_Error( 'invalid_post', 'Invalid post', 400 ); } $posts = get_posts( array( 'name' => $name, 'numberposts' => 1, 'post_type' => $this->_get_whitelisted_post_types(), ) ); if ( ! $posts || ! isset( $posts[0]->ID ) || ! $posts[0]->ID ) { $page = get_page_by_path( $name ); if ( ! $page ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } $post_id = $page->ID; } else { $post_id = (int) $posts[0]->ID; } return $post_id; } } class.wpcom-json-api-site-settings-v1-3-endpoint.php 0000644 00000021705 14722054026 0016335 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Site_Settings_V1_3_Endpoint( array( 'description' => 'Get detailed settings information about a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.3', 'method' => 'GET', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1.3/sites/en.blog.wordpress.com/settings?pretty=1', ) ); new WPCOM_JSON_API_Site_Settings_V1_3_Endpoint( array( 'description' => 'Update settings for a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.3', 'method' => 'POST', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'blogname' => '(string) Blog name', 'blogdescription' => '(string) Blog description', 'default_pingback_flag' => '(bool) Notify blogs linked from article?', 'default_ping_status' => '(bool) Allow link notifications from other blogs?', 'default_comment_status' => '(bool) Allow comments on new articles?', 'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines', 'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati', 'jetpack_relatedposts_enabled' => '(bool) Enable related posts?', 'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?', 'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?', 'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?', 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?', 'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface', 'jetpack_search_enabled' => '(bool) Enable Jetpack Search', 'jetpack_search_supported' => '(bool) Jetpack Search supported', 'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow', 'infinite_scroll' => '(bool) Support infinite scroll of posts?', 'default_category' => '(int) Default post category', 'default_post_format' => '(string) Default post format', 'require_name_email' => '(bool) Require comment authors to fill out name and email?', 'comment_registration' => '(bool) Require users to be registered and logged in to comment?', 'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?', 'close_comments_days_old' => '(int) Age at which to close comments', 'thread_comments' => '(bool) Enable threaded comments?', 'thread_comments_depth' => '(int) Depth to thread comments', 'page_comments' => '(bool) Break comments into pages?', 'comments_per_page' => '(int) Number of comments to display per page', 'default_comments_page' => '(string) newest|oldest Which page of comments to display first', 'comment_order' => '(string) asc|desc Order to display comments within page', 'comments_notify' => '(bool) Email me when someone comments?', 'moderation_notify' => '(bool) Email me when a comment is helf for moderation?', 'social_notifications_like' => '(bool) Email me when someone likes my post?', 'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?', 'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?', 'comment_moderation' => '(bool) Moderate comments for manual approval?', 'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?', 'comment_max_links' => '(int) Moderate comments that contain X or more links', 'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line', 'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line', 'lang_id' => '(int) ID for language blog is written in', 'locale' => '(string) locale code for language blog is written in', 'wga' => '(array) Google Analytics Settings', 'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?', 'disabled_reblogs' => '(bool) Are reblogs disabled on posts?', 'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?', 'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)', 'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"', 'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed', 'sharing_open_links' => '(string) Link target for sharing buttons (same or new)', 'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button', 'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.', 'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site', 'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'', 'gmt_offset' => '(int) Site offset from UTC in hours', 'date_format' => '(string) PHP Date-compatible date format', 'time_format' => '(string) PHP Date-compatible time format', 'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)', 'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site', 'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page', 'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site', 'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page', Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.', Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives', 'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook', 'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts', 'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear', 'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache', 'posts_per_page' => '(int) Number of posts to show on blog pages', 'posts_per_rss' => '(int) Number of posts to show in the RSS feed', 'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts', 'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts', 'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments', ), 'response_format' => array( 'updated' => '(array)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1', ) ); /** * Site settings v1_3 endpoint class. */ class WPCOM_JSON_API_Site_Settings_V1_3_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_2_Endpoint { /** * Get defaults. * * @return array */ protected function get_defaults() { return array( 'code' => '', 'anonymize_ip' => false, 'ec_track_purchases' => false, 'ec_track_add_to_cart' => false, ); } } class.wpcom-json-api-update-site-homepage-endpoint.php 0000644 00000006556 14722054026 0017065 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Set site homepage settings API endpoint. * * Endpoint: /sites/%s/homepage */ new WPCOM_JSON_API_Update_Site_Homepage_Endpoint( array( 'description' => 'Set site homepage settings', 'group' => '__do_not_document', 'stat' => 'sites:1:homepage', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/homepage', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'request_format' => array( 'is_page_on_front' => '(bool) True if we will use a page as the homepage; false to use a blog page as the homepage.', 'page_on_front_id' => '(int) Optional. The ID of the page to use as the homepage if is_page_on_front is true.', 'page_for_posts_id' => '(int) Optional. The ID of the page to use as the blog page if is_page_on_front is true.', ), 'response_format' => array( 'is_page_on_front' => '(bool) True if we will use a page as the homepage; false to use a blog page as the homepage.', 'page_on_front_id' => '(int) The ID of the page to use as the homepage if is_page_on_front is true.', 'page_for_posts_id' => '(int) The ID of the page to use as the blog page if is_page_on_front is true.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/homepage', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'is_page_on_front' => true, 'page_on_front_id' => 1, 'page_for_posts_id' => 0, ), ), 'example_response' => '{"is_page_on_front":true,"page_on_front_id":1,"page_for_posts_id":0}', ) ); /** * Site homepage setting endpoint class. */ class WPCOM_JSON_API_Update_Site_Homepage_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Set site homepage setting API callback. * * @param string $path API path. * @param int $site_id Blog ID. */ public function callback( $path = '', $site_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( 'unauthorized', 'User is not authorized to access homepage settings', 403 ); } $args = $this->input(); if ( empty( $args ) || ! is_array( $args ) ) { return $this->get_current_settings(); } if ( isset( $args['is_page_on_front'] ) ) { $show_on_front = $args['is_page_on_front'] ? 'page' : 'posts'; update_option( 'show_on_front', $show_on_front ); } if ( isset( $args['page_on_front_id'] ) ) { update_option( 'page_on_front', $args['page_on_front_id'] ); } if ( isset( $args['page_for_posts_id'] ) ) { update_option( 'page_for_posts', $args['page_for_posts_id'] ); } return $this->get_current_settings(); } /** * Get current site homepage settings. * * @return array */ public function get_current_settings() { $is_page_on_front = ( get_option( 'show_on_front' ) === 'page' ); $page_on_front_id = get_option( 'page_on_front' ); $page_for_posts_id = get_option( 'page_for_posts' ); return array( 'is_page_on_front' => $is_page_on_front, 'page_on_front_id' => $page_on_front_id, 'page_for_posts_id' => $page_for_posts_id, ); } } class.wpcom-json-api-site-settings-v1-2-endpoint.php 0000644 00000025625 14722054026 0016341 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Site_Settings_V1_2_Endpoint( array( 'description' => 'Get detailed settings information about a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.2', 'method' => 'GET', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/settings?pretty=1', ) ); new WPCOM_JSON_API_Site_Settings_V1_2_Endpoint( array( 'description' => 'Update settings for a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'blogname' => '(string) Blog name', 'blogdescription' => '(string) Blog description', 'default_pingback_flag' => '(bool) Notify blogs linked from article?', 'default_ping_status' => '(bool) Allow link notifications from other blogs?', 'default_comment_status' => '(bool) Allow comments on new articles?', 'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines', 'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati', 'jetpack_relatedposts_enabled' => '(bool) Enable related posts?', 'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?', 'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?', 'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?', 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?', 'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface', 'jetpack_search_enabled' => '(bool) Enable Jetpack Search', 'jetpack_search_supported' => '(bool) Jetpack Search supported', 'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow', 'infinite_scroll' => '(bool) Support infinite scroll of posts?', 'default_category' => '(int) Default post category', 'default_post_format' => '(string) Default post format', 'require_name_email' => '(bool) Require comment authors to fill out name and email?', 'comment_registration' => '(bool) Require users to be registered and logged in to comment?', 'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?', 'close_comments_days_old' => '(int) Age at which to close comments', 'thread_comments' => '(bool) Enable threaded comments?', 'thread_comments_depth' => '(int) Depth to thread comments', 'page_comments' => '(bool) Break comments into pages?', 'comments_per_page' => '(int) Number of comments to display per page', 'default_comments_page' => '(string) newest|oldest Which page of comments to display first', 'comment_order' => '(string) asc|desc Order to display comments within page', 'comments_notify' => '(bool) Email me when someone comments?', 'moderation_notify' => '(bool) Email me when a comment is helf for moderation?', 'social_notifications_like' => '(bool) Email me when someone likes my post?', 'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?', 'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?', 'comment_moderation' => '(bool) Moderate comments for manual approval?', 'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?', 'comment_max_links' => '(int) Moderate comments that contain X or more links', 'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line', 'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line', 'lang_id' => '(int) ID for language blog is written in', 'locale' => '(string) locale code for language blog is written in', 'wga' => '(array) Google Analytics Settings', 'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?', 'disabled_reblogs' => '(bool) Are reblogs disabled on posts?', 'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?', 'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)', 'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"', 'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed', 'sharing_open_links' => '(string) Link target for sharing buttons (same or new)', 'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button', 'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.', 'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site', 'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'', 'gmt_offset' => '(int) Site offset from UTC in hours', 'date_format' => '(string) PHP Date-compatible date format', 'time_format' => '(string) PHP Date-compatible time format', 'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)', 'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site', 'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page', 'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site', 'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page', Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.', Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives', 'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook', 'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts', 'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear', 'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache', 'posts_per_page' => '(int) Number of posts to show on blog pages', 'posts_per_rss' => '(int) Number of posts to show in the RSS feed', 'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts', 'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts', 'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments', ), 'response_format' => array( 'updated' => '(array)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1', ) ); /** * Site settings v1_2 endpoint. */ class WPCOM_JSON_API_Site_Settings_V1_2_Endpoint extends WPCOM_JSON_API_Site_Settings_Endpoint { /** * Site format. * * @var array */ public static $site_format = array( 'ID' => '(int) Site ID', 'name' => '(string) Title of site', 'description' => '(string) Tagline or description of site', 'URL' => '(string) Full URL to the site', 'locale' => '(string) Locale code of the site', 'locale_variant' => '(string) Locale variant code for the site, if set', 'settings' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site.', ); /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { add_filter( 'site_settings_endpoint_update_locale', array( $this, 'update_locale' ) ); add_filter( 'site_settings_endpoint_get', array( $this, 'return_locale' ) ); add_filter( 'site_settings_site_format', array( $this, 'site_format' ) ); return parent::callback( $path, $blog_id ); } /** * Get the locale. * * @param string $key - the key. * * @return string|false */ protected function get_locale( $key ) { if ( 'locale' === $key ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return (string) get_blog_lang_code(); } else { return get_locale(); } } return false; } /** * Return the locale. * * @param array $settings - the site settings. * * @return array `$settings` with a locale key added. */ public function return_locale( $settings ) { return $settings + array( 'locale' => $this->get_locale( 'locale' ) ); } /** * Update the locale. * * @param string $value - the locale code. * * @return bool */ public function update_locale( $value ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $lang_id = get_lang_id_by_code( $value ); if ( ! empty( $lang_id ) ) { if ( update_option( 'lang_id', $lang_id ) ) { return true; } } } return false; } /** * Format the site. * * @param string $format - the format. * * @return array */ public function site_format( $format ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable return self::$site_format; } } class.wpcom-json-api-list-users-endpoint.php 0000644 00000021133 14722054026 0015154 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List users endpoint. */ new WPCOM_JSON_API_List_Users_Endpoint( array( 'description' => 'List the users of a site.', 'group' => 'users', 'stat' => 'users:list', 'method' => 'GET', 'path' => '/sites/%s/users', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=20) Limit the total number of authors returned.', 'offset' => '(int=0) The first n authors to be skipped in the returned array.', 'order' => array( 'DESC' => 'Return authors in descending order.', 'ASC' => 'Return authors in ascending order.', ), 'order_by' => array( 'ID' => 'Order by ID (default).', 'login' => 'Order by username.', 'nicename' => 'Order by nicename.', 'email' => 'Order by author email address.', 'url' => 'Order by author URL.', 'registered' => 'Order by registered date.', 'display_name' => 'Order by display name.', 'post_count' => 'Order by number of posts published.', ), 'authors_only' => '(bool) Set to true to fetch authors only', 'include_viewers' => '(bool) Set to true to include viewers for Simple sites. When you pass in this parameter, order, order_by and search_columns are ignored. Currently, `search` is limited to the first page of results.', 'type' => "(string) Specify the post type to query authors for. Only works when combined with the `authors_only` flag. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'search' => '(string) Find matching users.', 'search_columns' => "(array) Specify which columns to check for matching users. Can be any of 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', and 'display_name'. Only works when combined with `search` parameter.", 'role' => '(string) Specify a specific user role to fetch.', 'capability' => '(string) Specify a specific capability to fetch. You can specify multiple by comma separating them, in which case the user needs to match all capabilities provided.', ), 'response_format' => array( 'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).', 'authors' => '(array:author) Array of author objects.', ), 'example_response' => '{ "found": 1, "users": [ { "ID": 78972699, "login": "apiexamples", "email": "justin+apiexamples@a8c.com", "name": "apiexamples", "first_name": "", "last_name": "", "nice_name": "apiexamples", "URL": "http://apiexamples.wordpress.com", "avatar_URL": "https://1.gravatar.com/avatar/a2afb7b6c0e23e5d363d8612fb1bd5ad?s=96&d=identicon&r=G", "profile_URL": "https://gravatar.com/apiexamples", "site_ID": 82974409, "roles": [ "administrator" ], "is_super_admin": false } ] }', 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/users', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List users endpoint class. * * /sites/%s/users/ -> $blog_id */ class WPCOM_JSON_API_List_Users_Endpoint extends WPCOM_JSON_API_Endpoint { /** * The response format. * * @var array */ public $response_format = array( 'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).', 'users' => '(array:author) Array of user objects', ); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $authors_only = ( ! empty( $args['authors_only'] ) ); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 1000 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 1000.', 400 ); } if ( $authors_only ) { if ( empty( $args['type'] ) ) { $args['type'] = 'post'; } if ( ! $this->is_post_type_allowed( $args['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } $post_type_object = get_post_type_object( $args['type'] ); if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) { return new WP_Error( 'unauthorized', 'User cannot view authors for specified post type', 403 ); } } elseif ( ! current_user_can( 'list_users' ) ) { return new WP_Error( 'unauthorized', 'User cannot view users for specified site', 403 ); } $query = array( 'number' => $args['number'], 'offset' => $args['offset'], 'order' => $args['order'], 'orderby' => $args['order_by'], 'fields' => 'ID', ); if ( $authors_only ) { $query['capability'] = array( 'edit_posts' ); } if ( ! empty( $args['search'] ) ) { $query['search'] = $args['search']; } if ( ! empty( $args['search_columns'] ) ) { // this `user_search_columns` filter is necessary because WP_User_Query does not allow `display_name` as a search column. $this->search_columns = array_intersect( $args['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', 'display_name' ) ); add_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ), 10, 3 ); } if ( ! empty( $args['role'] ) ) { $query['role'] = $args['role']; } if ( ! empty( $args['capability'] ) ) { $query['capability'] = $args['capability']; } $user_query = new WP_User_Query( $query ); remove_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ) ); $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM; $include_viewers = (bool) isset( $args['include_viewers'] ) && $args['include_viewers'] && $is_wpcom; $page = ( (int) ( $args['offset'] / $args['number'] ) ) + 1; $viewers = $include_viewers ? get_private_blog_users( $blog_id, array( 'page' => $page, 'per_page' => $args['number'], ) ) : array(); $viewers = array_map( array( $this, 'get_author' ), $viewers ); // we restrict search field to name when include_viewers is true. if ( $include_viewers && ! empty( $args['search'] ) ) { $viewers = array_filter( $viewers, function ( $viewer ) use ( $args ) { // remove special database search characters from search term $search_term = str_replace( '*', '', $args['search'] ); return strpos( $viewer->name, $search_term ) !== false; } ); } $return = array(); foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $user_count = (int) $user_query->get_total(); $viewer_count = 0; if ( $include_viewers ) { if ( empty( $args['search'] ) ) { $viewer_count = (int) get_count_private_blog_users( $blog_id ); } else { $viewer_count = count( $viewers ); } } $return[ $key ] = $user_count + $viewer_count; break; case 'users': $users = array(); $is_multisite = is_multisite(); foreach ( $user_query->get_results() as $u ) { $the_user = $this->get_author( $u, true ); if ( $the_user && ! is_wp_error( $the_user ) ) { $userdata = get_userdata( $u ); $the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array(); if ( $is_multisite ) { $the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' ); } $users[] = $the_user; } } $combined_users = array_merge( $users, $viewers ); // When viewers are included, we ignore the order & orderby parameters. if ( $include_viewers ) { usort( $combined_users, function ( $a, $b ) { return strcmp( strtolower( $a->name ), strtolower( $b->name ) ); } ); } $return[ $key ] = $combined_users; break; } } return $return; } /** * Override search columns. * * @param array $search_columns - the search column we're overriding. * @param array $search - the search query. */ public function api_user_override_search_columns( $search_columns, $search ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable return $this->search_columns; } } class.wpcom-json-api-update-post-v1-1-endpoint.php 0000644 00000134001 14722054026 0015770 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update post endpoint v1.1 * * Endpoints: * Create a post: /sites/%s/posts/new * Update a post: /sites/%s/posts/%d * Delete a post: /sites/%s/posts/%d/delete * Restore a post: /sites/%s/posts/%d/restore */ new WPCOM_JSON_API_Update_Post_v1_1_Endpoint( array( 'description' => 'Create a post.', 'group' => 'posts', 'stat' => 'posts:new', 'new_version' => '1.2', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( // explicitly document all input. 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'pending' => 'Mark the post as pending editorial approval.', 'future' => 'Schedule the post (alias for publish; you must also set a future date).', 'auto-draft' => 'Save a placeholder for a newly created post, with no content.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'type' => "(string) The post type. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'terms' => '(object) Mapping of taxonomy to comma-separated list or array of terms (name or id)', 'categories' => '(array|string) Comma-separated list or array of categories (name or id)', 'tags' => '(array|string) Comma-separated list or array of tags (name or id)', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint. Errors produced by media uploads, if any, will be in `media_errors` in the response. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image Post' \<br />--form 'media[0]=@/path/to/file.jpg' \<br />--form 'media_attrs[0][caption]=My Great Photo' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post. Errors produced by media sideloading, if any, will be in `media_errors` in the response.', 'media_attrs' => '(array) An array of attributes (`title`, `description` and `caption`) are supported to assign to the media uploaded via the `media` or `media_urls` properties. You must use a numeric index for the keys of `media_attrs` which follow the same sequence as `media` and `media_urls`. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Gallery Post' \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://exapmple.com/file2.jpg' \<br /> \<br />--form 'media_attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'media_attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', 'discussion' => '(object) A hash containing one or more of the following boolean values, which default to the blog\'s discussion preferences: `comments_open`, `pings_open`', 'likes_enabled' => "(bool) Should the post be open to likes? Defaults to the blog's preference.", 'sharing_enabled' => '(bool) Should sharing buttons show on this post? Defaults to true.', 'menu_order' => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.', 'page_template' => '(string) (Pages Only) The page template this page should use.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World', 'content' => 'Hello. I am a test post. I was created by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); new WPCOM_JSON_API_Update_Post_v1_1_Endpoint( array( 'description' => 'Edit a post.', 'group' => 'posts', 'stat' => 'posts:1:POST', 'new_version' => '1.2', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'request_format' => array( 'date' => "(ISO 8601 datetime) The post's creation time.", 'title' => '(HTML) The post title.', 'content' => '(HTML) The post content.', 'excerpt' => '(HTML) An optional post excerpt.', 'slug' => '(string) The name (slug) for the post, used in URLs.', 'author' => '(string) The username or ID for the user to assign the post to.', 'publicize' => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.', 'publicize_message' => '(string) Custom message to be shared to external services.', 'status' => array( 'publish' => 'Publish the post.', 'private' => 'Privately publish the post.', 'draft' => 'Save the post as a draft.', 'future' => 'Schedule the post (alias for publish; you must also set a future date).', 'pending' => 'Mark the post as pending editorial approval.', 'trash' => 'Set the post as trashed.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.', 'parent' => "(int) The post ID of the new post's parent.", 'terms' => '(object) Mapping of taxonomy to comma-separated list or array of terms (name or id)', 'categories' => '(array|string) Comma-separated list or array of categories (name or id)', 'tags' => '(array|string) Comma-separated list or array of tags (name or id)', 'format' => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ), 'discussion' => '(object) A hash containing one or more of the following boolean values, which default to the blog\'s discussion preferences: `comments_open`, `pings_open`', 'likes_enabled' => '(bool) Should the post be open to likes?', 'menu_order' => '(int) (Pages only) the order pages should appear in. Use 0 to maintain alphabetical order.', 'page_template' => '(string) (Pages Only) The page template this page should use.', 'sharing_enabled' => '(bool) Should sharing buttons show on this post?', 'featured_image' => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.', 'media' => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options resposne of the site endpoint. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", 'media_urls' => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.', 'metadata' => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with proper capabilities. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/881', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Hello World (Again)', 'content' => 'Hello. I am an edited post. I was edited by the API', 'tags' => 'tests', 'categories' => 'API', ), ), ) ); new WPCOM_JSON_API_Update_Post_v1_1_Endpoint( array( 'description' => 'Delete a post. Note: If the trash is enabled, this request will send the post to the trash. A second request will permanently delete the post.', 'group' => 'posts', 'stat' => 'posts:1:delete', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/$post_ID/delete/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); new WPCOM_JSON_API_Update_Post_v1_1_Endpoint( array( 'description' => 'Restore a post or page from the trash to its previous status.', 'group' => 'posts', 'stat' => 'posts:1:restore', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/%d/restore', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/$post_ID/restore/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Update post v1.1 endpoint class. */ class WPCOM_JSON_API_Update_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { /** * WPCOM_JSON_API_Update_Post_v1_1_Endpoint constructor. * * @param array $args Args. */ public function __construct( $args ) { parent::__construct( $args ); if ( $this->api->ends_with( $this->path, '/delete' ) ) { $this->post_object_format['status']['deleted'] = 'The post has been deleted permanently.'; } } /** * Update post API v1.1 callback. * * /sites/%s/posts/new -> $blog_id * /sites/%s/posts/%d -> $blog_id, $post_id * /sites/%s/posts/%d/delete -> $blog_id, $post_id * /sites/%s/posts/%d/restore -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. * * @return array|bool|WP_Error */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_post( $path, $blog_id, $post_id ); } elseif ( $this->api->ends_with( $path, '/restore' ) ) { return $this->restore_post( $path, $blog_id, $post_id ); } else { return $this->write_post( $path, $blog_id, $post_id ); } } /** * Create or update a post. * * /sites/%s/posts/new -> $blog_id * /sites/%s/posts/%d -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. */ public function write_post( $path, $blog_id, $post_id ) { $delete_featured_image = null; $media_results = array(); $post = null; global $wpdb; $new = $this->api->ends_with( $path, '/new' ); $args = $this->query_args(); // unhook publicize, it's hooked again later -- without this, skipping services is impossible. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { remove_action( 'save_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ), 100, 2 ); add_action( 'rest_api_inserted_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ) ); if ( $this->should_load_theme_functions( $post_id ) ) { $this->load_theme_functions(); } } if ( $new ) { $input = $this->input( true ); // 'future' is an alias for 'publish' for now if ( 'future' === $input['status'] ) { $input['status'] = 'publish'; } // default to post. if ( empty( $input['type'] ) ) { $input['type'] = 'post'; } if ( 'revision' === $input['type'] ) { if ( ! isset( $input['parent'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $input['status'] = 'inherit'; // force inherit for revision type. $input['slug'] = $input['parent'] . '-autosave-v1'; } elseif ( ! isset( $input['title'] ) && ! isset( $input['content'] ) && ! isset( $input['excerpt'] ) ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $post_type = get_post_type_object( $input['type'] ); if ( ! $this->is_post_type_allowed( $input['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } if ( ! empty( $input['author'] ) ) { $author_id = $this->parse_and_set_author( $input['author'], $input['type'] ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( 'publish' === $input['status'] ) { if ( ! current_user_can( $post_type->cap->publish_posts ) ) { if ( current_user_can( $post_type->cap->edit_posts ) ) { $input['status'] = 'pending'; } else { return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 ); } } } elseif ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 ); } } else { $input = $this->input( false ); if ( ! is_array( $input ) || ! $input ) { return new WP_Error( 'invalid_input', 'Invalid request input', 400 ); } $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( isset( $input['status'] ) && 'trash' === $input['status'] && ! current_user_can( 'delete_post', $post_id ) ) { return new WP_Error( 'unauthorized', 'User cannot delete post', 403 ); } // 'future' is an alias for 'publish' for now if ( isset( $input['status'] ) && 'future' === $input['status'] ) { $input['status'] = 'publish'; } $_post_type = ( ! empty( $input['type'] ) ) ? $input['type'] : $post->post_type; $post_type = get_post_type_object( $_post_type ); if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit post', 403 ); } if ( ! empty( $input['author'] ) ) { $author_id = $this->parse_and_set_author( $input['author'], $_post_type ); unset( $input['author'] ); if ( is_wp_error( $author_id ) ) { return $author_id; } } if ( ( isset( $input['status'] ) && 'publish' === $input['status'] ) && 'publish' !== $post->post_status && ! current_user_can( 'publish_post', $post->ID ) ) { $input['status'] = 'pending'; } $last_status = $post->post_status; $new_status = isset( $input['status'] ) ? $input['status'] : $last_status; // Make sure that drafts get the current date when transitioning to publish if not supplied in the post. // Similarly, scheduled posts that are manually published before their scheduled date should have the date reset. $date_in_past = ( strtotime( $post->post_date_gmt ) < time() ); $reset_draft_date = 'publish' === $new_status && 'draft' === $last_status && ! isset( $input['date_gmt'] ) && $date_in_past; $reset_scheduled_date = 'publish' === $new_status && 'future' === $last_status && ! isset( $input['date_gmt'] ) && ! $date_in_past; if ( $reset_draft_date || $reset_scheduled_date ) { $input['date_gmt'] = gmdate( 'Y-m-d H:i:s' ); } // Untrash a post so that the proper hooks get called as well as the comments get untrashed. if ( $this->should_untrash_post( $last_status, $new_status, $post ) ) { $input = $this->untrash_post( $post, $input ); } } if ( function_exists( 'wpcom_switch_to_blog_locale' ) ) { // fixes calypso-pre-oss #12476: respect blog locale when creating the post slug. wpcom_switch_to_blog_locale( $blog_id ); } // If date was set, $this->input will set date_gmt, date still needs to be adjusted for the blog's offset. if ( isset( $input['date_gmt'] ) ) { $gmt_offset = get_option( 'gmt_offset' ); $time_with_offset = strtotime( $input['date_gmt'] ) + $gmt_offset * HOUR_IN_SECONDS; $input['date'] = gmdate( 'Y-m-d H:i:s', $time_with_offset ); } if ( ! empty( $author_id ) && get_current_user_id() !== $author_id ) { if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'unauthorized', "User is not allowed to publish others' posts.", 403 ); } elseif ( ! user_can( $author_id, $post_type->cap->edit_posts ) ) { return new WP_Error( 'unauthorized', 'Assigned author cannot publish post.', 403 ); } } if ( ! is_post_type_hierarchical( $post_type->name ) && 'revision' !== $post_type->name ) { unset( $input['parent'] ); } $input['terms'] = isset( $input['terms'] ) ? (array) $input['terms'] : array(); // Convert comma-separated terms to array before attempting to // merge with hardcoded taxonomies. foreach ( $input['terms'] as $taxonomy => $terms ) { if ( is_string( $terms ) ) { $input['terms'][ $taxonomy ] = explode( ',', $terms ); } elseif ( ! is_array( $terms ) ) { $input['terms'][ $taxonomy ] = array(); } } // For each hard-coded taxonomy, merge into terms object. foreach ( array( 'categories' => 'category', 'tags' => 'post_tag', ) as $taxonomy_key => $taxonomy ) { if ( ! isset( $input[ $taxonomy_key ] ) ) { continue; } if ( ! isset( $input['terms'][ $taxonomy ] ) ) { $input['terms'][ $taxonomy ] = array(); } $terms = $input[ $taxonomy_key ]; if ( is_string( $terms ) ) { $terms = explode( ',', $terms ); } elseif ( ! is_array( $terms ) ) { continue; } $input['terms'][ $taxonomy ] = array_merge( $input['terms'][ $taxonomy ], $terms ); } $tax_input = array(); foreach ( $input['terms'] as $taxonomy => $terms ) { $tax_input[ $taxonomy ] = array(); $is_hierarchical = is_taxonomy_hierarchical( $taxonomy ); foreach ( $terms as $term ) { /** * `curl --data 'terms[category][]=123'` should be interpreted as a category ID, * not a category whose name is '123'. * * Consequence: To add a category/tag whose name is '123', the client must * first look up its ID. */ $term = (string) $term; // ctype_digit compat. if ( ctype_digit( $term ) ) { $term = (int) $term; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // A term ID that doesn't already exist. Ignore it: we don't know what name to give it. if ( is_int( $term ) ) { continue; } // only add a new tag/cat if the user has access to. $tax = get_taxonomy( $taxonomy ); // see https://core.trac.wordpress.org/ticket/26409 . if ( $is_hierarchical && ! current_user_can( $tax->cap->edit_terms ) ) { continue; } elseif ( ! current_user_can( $tax->cap->assign_terms ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( ! is_wp_error( $term_info ) ) { if ( $is_hierarchical ) { // Hierarchical terms must be added by ID. $tax_input[ $taxonomy ][] = (int) $term_info['term_id']; } elseif ( is_int( $term ) ) { // Non-hierarchical terms must be added by name. $term = get_term( $term, $taxonomy ); $tax_input[ $taxonomy ][] = $term->name; } else { $tax_input[ $taxonomy ][] = $term; } } } } if ( isset( $input['terms']['category'] ) && empty( $tax_input['category'] ) && 'revision' !== $post_type->name ) { $tax_input['category'][] = get_option( 'default_category' ); } unset( $input['terms'], $input['tags'], $input['categories'] ); $insert = array(); if ( ! empty( $input['slug'] ) ) { $insert['post_name'] = $input['slug']; unset( $input['slug'] ); } if ( isset( $input['discussion'] ) ) { $discussion = (array) $input['discussion']; foreach ( array( 'comment', 'ping' ) as $discussion_type ) { $discussion_open = sprintf( '%ss_open', $discussion_type ); $discussion_status = sprintf( '%s_status', $discussion_type ); if ( isset( $discussion[ $discussion_open ] ) ) { $is_open = WPCOM_JSON_API::is_truthy( $discussion[ $discussion_open ] ); $discussion[ $discussion_status ] = $is_open ? 'open' : 'closed'; } if ( in_array( $discussion[ $discussion_status ], array( 'open', 'closed' ), true ) ) { $insert[ $discussion_status ] = $discussion[ $discussion_status ]; } } } unset( $input['discussion'] ); if ( isset( $input['menu_order'] ) ) { $insert['menu_order'] = $input['menu_order']; unset( $input['menu_order'] ); } $publicize = isset( $input['publicize'] ) ? $input['publicize'] : null; unset( $input['publicize'] ); $publicize_custom_message = isset( $input['publicize_message'] ) ? $input['publicize_message'] : null; unset( $input['publicize_message'] ); if ( isset( $input['featured_image'] ) ) { $featured_image = trim( $input['featured_image'] ); $delete_featured_image = empty( $featured_image ); unset( $input['featured_image'] ); } $metadata = isset( $input['metadata'] ) ? $input['metadata'] : null; unset( $input['metadata'] ); $likes = isset( $input['likes_enabled'] ) ? $input['likes_enabled'] : null; unset( $input['likes_enabled'] ); $sharing = isset( $input['sharing_enabled'] ) ? $input['sharing_enabled'] : null; unset( $input['sharing_enabled'] ); $sticky = isset( $input['sticky'] ) ? $input['sticky'] : null; unset( $input['sticky'] ); foreach ( $input as $key => $value ) { $insert[ "post_$key" ] = $value; } if ( ! empty( $author_id ) ) { $insert['post_author'] = absint( $author_id ); } if ( ! empty( $tax_input ) ) { $insert['tax_input'] = $tax_input; } $has_media = ! empty( $input['media'] ) ? count( $input['media'] ) : false; $has_media_by_url = ! empty( $input['media_urls'] ) ? count( $input['media_urls'] ) : false; $media_id_string = ''; if ( $has_media || $has_media_by_url ) { $media_files = ! empty( $input['media'] ) ? $input['media'] : array(); $media_urls = ! empty( $input['media_urls'] ) ? $input['media_urls'] : array(); $media_attrs = ! empty( $input['media_attrs'] ) ? $input['media_attrs'] : array(); $media_results = $this->handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs ); $media_id_string = implode( ',', array_filter( array_map( 'absint', $media_results['media_ids'] ) ) ); } if ( $new ) { if ( isset( $input['content'] ) && ! has_shortcode( $input['content'], 'gallery' ) && ( $has_media || $has_media_by_url ) ) { switch ( ( $has_media + $has_media_by_url ) ) { case 0: // No images - do nothing. break; case 1: // 1 image - make it big $input['content'] = sprintf( "[gallery size=full ids='%s' columns=1]\n\n", $media_id_string ) . $input['content']; $insert['post_content'] = $input['content']; break; default: // Several images - 3 column gallery. $input['content'] = sprintf( "[gallery ids='%s']\n\n", $media_id_string ) . $input['content']; $insert['post_content'] = $input['content']; break; } } $post_id = wp_insert_post( add_magic_quotes( $insert ), true ); } else { $insert['ID'] = $post->ID; // wp_update_post ignores date unless edit_date is set // See: https://codex.wordpress.org/Function_Reference/wp_update_post#Scheduling_posts . // See: https://core.trac.wordpress.org/browser/tags/3.9.2/src/wp-includes/post.php#L3302 . if ( isset( $input['date_gmt'] ) || isset( $input['date'] ) ) { $insert['edit_date'] = true; } // this two-step process ensures any changes submitted along with status=trash get saved before trashing. if ( isset( $input['status'] ) && 'trash' === $input['status'] ) { // if we insert it with status='trash', it will get double-trashed, so insert it as a draft first. unset( $insert['status'] ); $post_id = wp_update_post( (object) $insert ); // now call wp_trash_post so post_meta gets set and any filters get called. wp_trash_post( $post_id ); } else { $post_id = wp_update_post( (object) $insert ); } } if ( ! $post_id || is_wp_error( $post_id ) ) { return $post_id; } // make sure this post actually exists and is not an error of some kind (ie, trying to load media in the posts endpoint). $post_check = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( is_wp_error( $post_check ) ) { return $post_check; } if ( $media_id_string ) { // Yes - this is really how wp-admin does it. $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_parent = %d WHERE post_type = 'attachment' AND ID IN ( $media_id_string )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IDs are filtered to absint above. $post_id ) ); foreach ( $media_results['media_ids'] as $media_id ) { clean_attachment_cache( $media_id ); } clean_post_cache( $post_id ); } // set page template for this post. if ( isset( $input['page_template'] ) && 'page' === $post_type->name ) { $page_template = $input['page_template']; $page_templates = wp_get_theme()->get_page_templates( get_post( $post_id ) ); if ( empty( $page_template ) || 'default' === $page_template || isset( $page_templates[ $page_template ] ) ) { update_post_meta( $post_id, '_wp_page_template', $page_template ); } } // Set like status for the post. /** This filter is documented in modules/likes.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); if ( $new ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( isset( $likes ) ) { if ( $sitewide_likes_enabled ) { if ( false === $likes ) { update_post_meta( $post_id, 'switch_like_status', 0 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } elseif ( true === $likes ) { update_post_meta( $post_id, 'switch_like_status', 1 ); } else { delete_post_meta( $post_id, 'switch_like_status' ); } } // Set sharing status of the post. if ( $new ) { $sharing_enabled = isset( $sharing ) ? (bool) $sharing : true; if ( false === $sharing_enabled ) { update_post_meta( $post_id, 'sharing_disabled', 1 ); } } elseif ( isset( $sharing ) && true === $sharing ) { delete_post_meta( $post_id, 'sharing_disabled' ); } elseif ( isset( $sharing ) && false == $sharing ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual update_post_meta( $post_id, 'sharing_disabled', 1 ); } if ( isset( $sticky ) ) { if ( true === $sticky ) { stick_post( $post_id ); } else { unstick_post( $post_id ); } } // WPCOM Specific (Jetpack's will get bumped elsewhere // Tracks how many posts are published and sets meta // so we can track some other cool stats (like likes & comments on posts published). if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( ( $new && 'publish' === $input['status'] ) || ( ! $new && isset( $last_status ) && 'publish' !== $last_status && isset( $new_status ) && 'publish' === $new_status ) ) { /** This action is documented in modules/widgets/social-media-icons.php */ do_action( 'jetpack_bump_stats_extras', 'api-insights-posts', $this->api->token_details['client_id'] ); update_post_meta( $post_id, '_rest_api_published', 1 ); update_post_meta( $post_id, '_rest_api_client_id', $this->api->token_details['client_id'] ); } } // We ask the user/dev to pass Publicize services he/she wants activated for the post, but Publicize expects us // to instead flag the ones we don't want to be skipped. proceed with said logic. // Any posts coming from Path (client ID 25952) should also not publicize. if ( false === $publicize || ( isset( $this->api->token_details['client_id'] ) && 25952 === (int) $this->api->token_details['client_id'] ) ) { // No publicize at all, skip all by ID. foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); if ( ! $service_connections ) { continue; } foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } } elseif ( is_array( $publicize ) && ( $publicize !== array() ) ) { foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) { /* * We support both indexed and associative arrays: * * indexed are to pass entire services * * associative are to pass specific connections per service * * We do support mixed arrays: mixed integer and string keys (see 3rd example below). * * EG: array( 'linkedin', 'facebook') will only publicize to those, ignoring the other available services * Form data: publicize[]=linkedin&publicize[]=facebook * EG: array( 'linkedin' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two LinkedIn accounts, and one Facebook connection, of potentially many. * Form data: publicize[linkedin]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7 * EG: array( 'linkedin', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available LinkedIn accounts, but only 2 of potentially many Facebook connections * Form data: publicize[]=linkedin&publicize[facebook]=$pub_conn_id_0,$pub_conn_id_3 */ // Delete any stale SKIP value for the service by name. We'll add it back by ID. delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name ); // Get the user's connections. $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name ); // if the user doesn't have any connections for this service, move on. if ( ! $service_connections ) { continue; } if ( ! in_array( $name, $publicize, true ) && ! array_key_exists( $name, $publicize ) ) { // Skip the whole service by adding each connection ID. foreach ( $service_connections as $service_connection ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } } elseif ( ! empty( $publicize[ $name ] ) ) { // Seems we're being asked to only push to [a] specific connection[s]. // Explode the list on commas, which will also support a single passed ID. $requested_connections = explode( ',', ( preg_replace( '/[\s]*/', '', $publicize[ $name ] ) ) ); // Flag the connections we can't match with the requested list to be skipped. foreach ( $service_connections as $service_connection ) { if ( ! in_array( $service_connection->meta['connection_data']->id, $requested_connections, true ) ) { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 ); } else { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } else { // delete all SKIP values; it's okay to publish to all connected IDs for this service. foreach ( $service_connections as $service_connection ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id ); } } } } if ( $publicize_custom_message !== null ) { if ( empty( $publicize_custom_message ) ) { delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS ); } else { update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) ); } } if ( ! empty( $insert['post_format'] ) ) { if ( 'default' !== strtolower( $insert['post_format'] ) ) { set_post_format( $post_id, $insert['post_format'] ); } else { set_post_format( $post_id, get_option( 'default_post_format' ) ); } } if ( isset( $featured_image ) ) { $this->parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ); } if ( ! empty( $metadata ) ) { foreach ( (array) $metadata as $meta ) { $meta = (object) $meta; if ( in_array( $meta->key, Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) && ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 ); } $existing_meta_item = new stdClass(); if ( empty( $meta->operation ) ) { $meta->operation = 'update'; } if ( ! empty( $meta->value ) ) { if ( 'true' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = true; } if ( 'false' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $meta->value = false; } } if ( ! empty( $meta->id ) ) { $meta->id = absint( $meta->id ); $existing_meta_item = get_metadata_by_mid( 'post', $meta->id ); if ( $post_id !== (int) $existing_meta_item->post_id ) { // Only allow updates for metadata on this post. continue; } } $unslashed_meta_key = wp_unslash( $meta->key ); // should match what the final key will be. $meta->key = wp_slash( $meta->key ); $unslashed_existing_meta_key = wp_unslash( $existing_meta_item->meta_key ); $existing_meta_item->meta_key = wp_slash( $existing_meta_item->meta_key ); // make sure that the meta id passed matches the existing meta key. if ( ! empty( $meta->id ) && ! empty( $meta->key ) ) { $meta_by_id = get_metadata_by_mid( 'post', $meta->id ); if ( $meta_by_id->meta_key !== $meta->key ) { continue; // skip this meta. } } switch ( $meta->operation ) { case 'delete': if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) { delete_metadata_by_mid( 'post', $meta->id ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) { delete_post_meta( $post_id, $meta->key ); } break; case 'add': if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) { break; } elseif ( ! empty( $meta->key ) && ! empty( $meta->value ) && ( current_user_can( 'add_post_meta', $post_id, $unslashed_meta_key ) ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) { add_post_meta( $post_id, $meta->key, $meta->value ); } break; case 'update': if ( ! isset( $meta->value ) ) { break; } elseif ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_existing_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_metadata_by_mid( 'post', $meta->id, $meta->value ); } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value, $meta->previous_value ); } elseif ( ! empty( $meta->key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) { update_post_meta( $post_id, $meta->key, $meta->value ); } break; } } } /** This action is documented in json-endpoints/class.wpcom-json-api-update-post-endpoint.php */ do_action( 'rest_api_inserted_post', $post_id, $insert, $new ); $return = $this->get_post_by( 'ID', $post_id, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } if ( isset( $input['type'] ) && 'revision' === $input['type'] ) { $return['preview_nonce'] = wp_create_nonce( 'post_preview_' . $input['parent'] ); } if ( isset( $sticky ) ) { // workaround for sticky test occasionally failing, maybe a race condition with stick_post() above. $return['sticky'] = ( true === $sticky ); } if ( ! empty( $media_results['errors'] ) ) { $return['media_errors'] = $media_results['errors']; } if ( 'publish' !== $post->post_status ) { $sal_site = $this->get_sal_post_by( 'ID', $post_id, $args['context'] ); $return['other_URLs'] = (object) $sal_site->get_permalink_suggestions( $input['title'] ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); return $return; } /** * Delete a post. * * /sites/%s/posts/%d/delete -> $blog_id, $post_id * * @param string $path API path. * @param array $blog_id Blog ID. * @param array $post_id Post ID. * * @return array|WP_Error */ public function delete_post( $path, $blog_id, $post_id ) { $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! $this->is_post_type_allowed( $post->post_type ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } if ( ! current_user_can( 'delete_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot delete posts', 403 ); } $args = $this->query_args(); $return = $this->get_post_by( 'ID', $post->ID, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); // we need to call wp_trash_post so that untrash will work correctly for all post types. if ( 'trash' === $post->post_status ) { wp_delete_post( $post->ID ); } else { wp_trash_post( $post->ID ); } $status = get_post_status( $post->ID ); if ( false === $status ) { $return['status'] = 'deleted'; return $return; } return $this->get_post_by( 'ID', $post->ID, $args['context'] ); } /** * Restore a post. * * /sites/%s/posts/%d/restore -> $blog_id, $post_id * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $post_id Post ID. * * @return array|WP_Error */ public function restore_post( $path, $blog_id, $post_id ) { $args = $this->query_args(); $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } if ( ! current_user_can( 'delete_post', $post->ID ) ) { return new WP_Error( 'unauthorized', 'User cannot restore trashed posts', 403 ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); wp_untrash_post( $post->ID ); return $this->get_post_by( 'ID', $post->ID, $args['context'] ); } /** * Set or delete a post's featured image. * * @param int $post_id Post ID. * @param bool $delete_featured_image Whether to delete the featured image. * @param int $featured_image Thumbnail ID to attach. * * @return null|int|bool */ protected function parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ) { if ( $delete_featured_image ) { delete_post_thumbnail( $post_id ); return; } $featured_image = (string) $featured_image; // if we got a post ID, we can just set it as the thumbnail. if ( ctype_digit( $featured_image ) && 'attachment' === get_post_type( $featured_image ) ) { set_post_thumbnail( $post_id, $featured_image ); return $featured_image; } $featured_image_id = $this->handle_media_sideload( $featured_image, $post_id, 'image' ); if ( empty( $featured_image_id ) || ! is_int( $featured_image_id ) ) { return false; } set_post_thumbnail( $post_id, $featured_image_id ); return $featured_image_id; } /** * Get the Author ID for a post. * * @param int|string $author Author ID. * @param string $post_type Post type. * * @return int|WP_Error */ protected function parse_and_set_author( $author = null, $post_type = 'post' ) { if ( empty( $author ) || ! post_type_supports( $post_type, 'author' ) ) { return get_current_user_id(); } $author = (string) $author; if ( ctype_digit( $author ) ) { $_user = get_user_by( 'id', $author ); if ( ! $_user || is_wp_error( $_user ) ) { return new WP_Error( 'invalid_author', 'Invalid author provided' ); } return $_user->ID; } $_user = get_user_by( 'login', $author ); if ( ! $_user || is_wp_error( $_user ) ) { return new WP_Error( 'invalid_author', 'Invalid author provided' ); } return $_user->ID; } /** * Determine if a post can be untrashed. * * @param string $last_status Last post status. * @param string $new_status New post status. * @param WP_Post $post Post. * * @return bool */ protected function should_untrash_post( $last_status, $new_status, $post ) { return 'trash' === $last_status && 'trash' !== $new_status && isset( $post->ID ); } /** * Untrash a post. * * @param WP_Post $post Post to untrash. * @param array $input POST body data. */ protected function untrash_post( $post, $input ) { wp_untrash_post( $post->ID ); $untrashed_post = get_post( $post->ID ); // Lets make sure that we use the reverted the slug. if ( isset( $untrashed_post->post_name ) && $untrashed_post->post_name . '__trashed' === $input['slug'] ) { unset( $input['slug'] ); } return $input; } /** * Determine if a theme's functions.php file should be loaded. * * @param int $post_id Post ID. * * @return bool */ protected function should_load_theme_functions( $post_id = null ) { if ( empty( $post_id ) ) { $input = $this->input( true ); $type = $input['type']; } else { $type = get_post_type( $post_id ); } return ! empty( $type ) && ! in_array( $type, array( 'post', 'revision' ), true ); } } class.wpcom-json-api-update-media-endpoint.php 0000644 00000005672 14722054026 0015413 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update media item info endpoint. * * Endpoint: /sites/%s/media/%d */ new WPCOM_JSON_API_Update_Media_Endpoint( array( 'description' => 'Edit basic information about a media item.', 'group' => 'media', 'stat' => 'media:1:POST', 'method' => 'POST', 'path' => '/sites/%s/media/%d', 'deprecated' => true, 'max_version' => '1', 'new_version' => '1.1', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'request_format' => array( 'title' => '(string) The file name.', 'caption' => '(string) File caption.', 'description' => '(HTML) Description of the file.', ), 'response_format' => array( 'id' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'parent' => '(int) ID of the post this media is attached to', 'link' => '(string) URL to the file', 'title' => '(string) File name', 'caption' => '(string) User provided caption of the file', 'description' => '(string) Description of the file', 'metadata' => '(array) Array of metadata about the file, such as Exif data or sizes', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/446', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Updated Title', ), ), ) ); /** * Update media item info class. */ class WPCOM_JSON_API_Update_Media_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Update media item info API callback. * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $media_id Media ID. * * @return object|WP_Error */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'upload_files', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $item = $this->get_media_item( $media_id ); if ( is_wp_error( $item ) ) { return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); } $input = $this->input( true ); $insert = array(); if ( ! empty( $input['title'] ) ) { $insert['post_title'] = $input['title']; } if ( ! empty( $input['caption'] ) ) { $insert['post_excerpt'] = $input['caption']; } if ( ! empty( $input['description'] ) ) { $insert['post_content'] = $input['description']; } $insert['ID'] = $media_id; wp_update_post( (object) $insert ); $item = $this->get_media_item( $media_id ); return $item; } } class.wpcom-json-api-get-post-v1-1-endpoint.php 0000644 00000007173 14722054026 0015276 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Post_v1_1_Endpoint( array( 'description' => 'Get a single post (by ID).', 'min_version' => '1.1', 'max_version' => '1.1', 'group' => 'posts', 'stat' => 'posts:1', 'method' => 'GET', 'path' => '/sites/%s/posts/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/7', ) ); new WPCOM_JSON_API_Get_Post_v1_1_Endpoint( array( 'description' => 'Get a single post (by slug).', 'min_version' => '1.1', 'max_version' => '1.1', 'group' => 'posts', 'stat' => 'posts:slug', 'method' => 'GET', 'path' => '/sites/%s/posts/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_slug' => '(string) The post slug (a.k.a. sanitized name)', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff', ) ); /** * Get Post v1_1 endpoint. */ class WPCOM_JSON_API_Get_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { // phpcs:ignore /** * * API callback. * * /sites/%s/posts/%d -> $blog_id, $post_id * /sites/%s/posts/slug:%s -> $blog_id, $post_id * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $post_id - the post ID. */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); if ( str_contains( $path, '/posts/slug:' ) ) { $site = $this->get_platform()->get_site( $blog_id ); $post_id = $site->get_post_id_by_name( $post_id ); if ( is_wp_error( $post_id ) ) { return $post_id; } } return $this->fetch_post( $blog_id, $post_id, $args['context'] ); } /** * Helper function to fetch the content of a post. User validation * should be handled by the caller. * * @param int $blog_id The blog ID for the post. * @param int $post_id The post ID. * @param string $context The context we're fetching for. * @return array|SAL_Post|WP_Error */ public function fetch_post( $blog_id, $post_id, $context ) { $site = $this->get_platform()->get_site( $blog_id ); if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! in_array( get_post_type( $post_id ), array( false, 'post', 'revision' ), true ) ) { $this->load_theme_functions(); } $post = $this->get_post_by( 'ID', $post_id, $context ); if ( ! $post || is_wp_error( $post ) ) { return $post; } if ( ! $site->current_user_can_access_post_type( $post['type'], $context ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts' ); return $post; } } class.wpcom-json-api-get-taxonomy-endpoint.php 0000644 00000005115 14722054026 0015477 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Taxonomy_Endpoint( array( 'description' => 'Get information about a single category.', 'group' => 'taxonomy', 'stat' => 'categories:1', 'method' => 'GET', 'path' => '/sites/%s/categories/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$category' => '(string) The category slug', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/categories/slug:community', ) ); new WPCOM_JSON_API_Get_Taxonomy_Endpoint( array( 'description' => 'Get information about a single tag.', 'group' => 'taxonomy', 'stat' => 'tags:1', 'method' => 'GET', 'path' => '/sites/%s/tags/slug:%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$tag' => '(string) The tag slug', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/slug:wordpresscom', ) ); /** * GET Taxonomy endpoint class. */ class WPCOM_JSON_API_Get_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint { /** * * API callback. * * /sites/%s/tags/slug:%s -> $blog_id, $tag_id * /sites/%s/categories/slug:%s -> $blog_id, $tag_id * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $taxonomy_id - the taxonomy ID. */ public function callback( $path = '', $blog_id = 0, $taxonomy_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); if ( preg_match( '#/tags/#i', $path ) ) { $taxonomy_type = 'post_tag'; } else { $taxonomy_type = 'category'; } $return = $this->get_taxonomy( $taxonomy_id, $taxonomy_type, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'taxonomies' ); return $return; } } class.wpcom-json-api-add-widget-endpoint.php 0000644 00000011220 14722054026 0015047 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Activate a widget on a site. * * Endpoint: https://public-api.wordpress.com/rest/v1.1/sites/$site/widgets/new */ new WPCOM_JSON_API_Add_Widgets_Endpoint( array( 'description' => 'Activate a group of widgets on a site. The bulk version of using the /new endpoint', 'group' => '__do_not_document', 'stat' => 'widgets:new:bulk', 'force' => 'wpcom', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/widgets', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'request_format' => array( 'widgets' => '(array:widget) An array of widget objects to add.', ), 'response_format' => array( 'widgets' => '(array:widget) An array of widget objects added.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/widgets', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'id_base' => 'text', 'sidebar' => 'sidebar-2', 'position' => '0', 'settings' => array( 'title' => 'hello world' ), ), ), 'example_response' => ' { "id": "text-3", "id_base": "text", "settings": { "title": "hello world" }, "sidebar": "sidebar-2", "position": 0 }', ) ); new WPCOM_JSON_API_Add_Widgets_Endpoint( array( 'description' => 'Activate a widget on a site.', 'group' => 'sites', 'stat' => 'widgets:new', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/widgets/new', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'request_format' => array( 'id_base' => '(string) The base ID of the widget.', 'sidebar' => '(string) Optional. The ID of the sidebar where this widget will be active. If empty, the widget will be added in the first sidebar available.', 'position' => '(int) Optional. The position of the widget in the sidebar.', 'settings' => '(object) Optional. The settings for the new widget.', ), 'response_format' => array( 'id' => '(string) The actual ID of the widget.', 'sidebar' => '(string) The ID of the sidebar where this widget will be active.', 'position' => '(int) The final position of the widget in the sidebar.', 'settings' => '(array) The settings for the new widget.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/widgets/new', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'id_base' => 'text', 'sidebar' => 'sidebar-2', 'position' => '0', 'settings' => array( 'title' => 'hello world' ), ), ), 'example_response' => ' { "id": "text-3", "id_base": "text", "settings": { "title": "hello world" }, "sidebar": "sidebar-2", "position": 0 }', ) ); /** * The Add Widgets endpoint class. */ class WPCOM_JSON_API_Add_Widgets_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @uses Jetpack_Widgets * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { // Switch to the given blog. $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( 'unauthorized', 'User is not authorized to access widgets', 403 ); } require_once JETPACK__PLUGIN_DIR . '_inc/lib/widgets.php'; $args = $this->input( false, false ); // Don't filter the input. if ( empty( $args ) || ! is_array( $args ) ) { return new WP_Error( 'no_data', 'No data was provided.', 400 ); } if ( isset( $args['widgets'] ) || ! empty( $args['widgets'] ) ) { $widgets = Jetpack_Widgets::activate_widgets( $args['widgets'] ); if ( is_wp_error( $widgets ) ) { return $widgets; } return array( 'widgets' => $widgets ); } if ( ! isset( $args['id_base'] ) ) { return new WP_Error( 'missing_data', 'The data you provided was not accurate.', 400 ); } if ( empty( $args['sidebar'] ) ) { $active_sidebars = Jetpack_Widgets::get_active_sidebars(); reset( $active_sidebars ); $args['sidebar'] = key( $active_sidebars ); } return Jetpack_Widgets::activate_widget( $args['id_base'], $args['sidebar'], $args['position'], $args['settings'] ); } } class.wpcom-json-api-list-comments-endpoint.php 0000644 00000027203 14722054026 0015644 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Comments Walker Class. */ class WPCOM_JSON_API_List_Comments_Walker extends Walker { /** * Tree type. * * @var string */ public $tree_type = 'comment'; /** * Database fields. * * @var array */ public $db_fields = array( 'parent' => 'comment_parent', 'id' => 'comment_ID', ); /** * Start the element output. * * @param array $output - the output. * @param object $object - the object. * @param int $depth - depth. * @param array $args - the arguments. * @param int $current_object_id - the object ID. */ public function start_el( &$output, $object, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( ! is_array( $output ) ) { $output = array(); } $output[] = $object->comment_ID; } /** * Taken from WordPress's Walker_Comment::display_element() * * This function is designed to enhance Walker::display_element() to * display children of higher nesting levels than selected inline on * the highest depth level displayed. This prevents them being orphaned * at the end of the comment list. * * Example: max_depth = 2, with 5 levels of nested content. * 1 * 1.1 * 1.1.1 * 1.1.1.1 * 1.1.1.1.1 * 1.1.2 * 1.1.2.1 * 2 * 2.2 * * @see Walker_Comment::display_element() * @see Walker::display_element() * @see wp_list_comments() * * @param object $element — Data object. * @param array $children_elements - List of elements to continue traversing (passed by reference). * @param int $max_depth — Max depth to traverse. * @param int $depth — Depth of current element. * @param array $args — An array of arguments. * @param string $output — Used to append additional content (passed by reference). */ public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { if ( ! $element ) { return; } $id_field = $this->db_fields['id']; $id = $element->$id_field; parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); // If we're at the max depth, and the current element still has children, loop over those and display them at this level // This is to prevent them being orphaned to the end of the list. if ( $max_depth <= $depth + 1 && isset( $children_elements[ $id ] ) ) { foreach ( $children_elements[ $id ] as $child ) { $this->display_element( $child, $children_elements, $max_depth, $depth, $args, $output ); } unset( $children_elements[ $id ] ); } } } new WPCOM_JSON_API_List_Comments_Endpoint( array( 'description' => 'Get a list of recent comments.', 'group' => 'comments', 'stat' => 'comments', 'method' => 'GET', 'path' => '/sites/%s/comments/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/?number=2', ) ); new WPCOM_JSON_API_List_Comments_Endpoint( array( 'description' => 'Get a list of recent comments on a post.', 'group' => 'comments', 'stat' => 'posts:1:replies', 'method' => 'GET', 'path' => '/sites/%s/posts/%d/replies/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$post_ID' => '(int) The post ID', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/replies/?number=2', ) ); /** * List comment endpoint. * * /sites/%s/comments/ -> $blog_id * /sites/%s/posts/%d/replies/ -> $blog_id, $post_id * /sites/%s/comments/%d/replies/ -> $blog_id, $comment_id * * @todo permissions */ class WPCOM_JSON_API_List_Comments_Endpoint extends WPCOM_JSON_API_Comment_Endpoint { // phpcs:ignore /** * The response format. * * @var array */ public $response_format = array( 'found' => '(int) The total number of comments found that match the request (ignoring limits, offsets, and pagination).', 'site_ID' => '(int) The site ID', 'comments' => '(array:comment) An array of comment objects.', ); /** * Constructor function. * * @param array $args - the arguments. */ public function __construct( $args ) { parent::__construct( $args ); $this->query = array_merge( $this->query, array( 'number' => '(int=20) The number of comments to return. Limit: 100. When using hierarchical=1, number refers to the number of top-level comments returned.', 'offset' => '(int=0) 0-indexed offset. Not available if using hierarchical=1.', 'page' => '(int) Return the Nth 1-indexed page of comments. Takes precedence over the <code>offset</code> parameter. When using hierarchical=1, pagination is a bit different. See the note on the number parameter.', 'order' => array( 'DESC' => 'Return comments in descending order from newest to oldest.', 'ASC' => 'Return comments in ascending order from oldest to newest.', ), 'hierarchical' => array( 'false' => '', 'true' => '(BETA) Order the comment list hierarchically.', ), 'after' => '(ISO 8601 datetime) Return comments dated on or after the specified datetime. Not available if using hierarchical=1.', 'before' => '(ISO 8601 datetime) Return comments dated on or before the specified datetime. Not available if using hierarchical=1.', 'type' => array( 'any' => 'Return all comments regardless of type.', 'comment' => 'Return only regular comments.', 'trackback' => 'Return only trackbacks.', 'pingback' => 'Return only pingbacks.', 'pings' => 'Return both trackbacks and pingbacks.', ), 'status' => array( 'approved' => 'Return only approved comments.', 'unapproved' => 'Return only comments in the moderation queue.', 'spam' => 'Return only comments marked as spam.', 'trash' => 'Return only comments in the trash.', 'all' => 'Return comments of all statuses.', ), ) ); } /** * The callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $object_id - the object ID. */ public function callback( $path = '', $blog_id = 0, $object_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } if ( str_contains( $path, '/posts/' ) ) { // We're looking for comments of a particular post. $post_id = $object_id; $comment_id = 0; } else { // We're looking for comments for the whole blog, or replies to a single comment. $comment_id = $object_id; $post_id = 0; } // We can't efficiently get the number of replies to a single comment. $count = false; $found = -1; if ( ! $comment_id ) { // We can get comment counts for the whole site or for a single post, but only for certain queries. if ( 'any' === $args['type'] && ! isset( $args['after'] ) && ! isset( $args['before'] ) ) { $count = $this->api->wp_count_comments( $post_id ); } } switch ( $args['status'] ) { case 'approved': $status = 'approve'; if ( $count ) { $found = $count->approved; } break; default: if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'User cannot read non-approved comments', 403 ); } if ( 'unapproved' === $args['status'] ) { $status = 'hold'; $count_status = 'moderated'; } elseif ( 'all' === $args['status'] ) { $status = 'all'; $count_status = 'total_comments'; } else { $status = $args['status']; $count_status = $args['status']; } if ( $count ) { $found = $count->$count_status; } } /** This filter is documented in class.json-api.php */ $exclude = apply_filters( 'jetpack_api_exclude_comment_types', array( 'order_note', 'webhook_delivery', 'review', 'action_log' ) ); $query = array( 'order' => $args['order'], 'type' => 'any' === $args['type'] ? false : $args['type'], 'status' => $status, 'type__not_in' => $exclude, ); if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } } elseif ( $args['offset'] < 0 ) { $args['offset'] = 0; } if ( ! $args['hierarchical'] ) { $query['number'] = $args['number']; if ( isset( $args['page'] ) ) { $query['offset'] = ( $args['page'] - 1 ) * $args['number']; } else { $query['offset'] = $args['offset']; } $is_before = isset( $args['before_gmt'] ); $is_after = isset( $args['after_gmt'] ); if ( $is_before || $is_after ) { $query['date_query'] = array( 'column' => 'comment_date_gmt', 'inclusive' => true, ); if ( $is_before ) { $query['date_query']['before'] = $args['before_gmt']; } if ( $is_after ) { $query['date_query']['after'] = $args['after_gmt']; } } } if ( $args['hierarchical'] && $found > 5000 ) { // Massive comment thread found; don't pre-load comment metadata to reduce memory used. $query['update_comment_meta_cache'] = false; } if ( $post_id ) { $post = get_post( $post_id ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } $query['post_id'] = $post->ID; if ( $this->api->ends_with( $this->path, '/replies' ) ) { $query['parent'] = 0; } } elseif ( $comment_id ) { $comment = get_comment( $comment_id ); if ( ! $comment || is_wp_error( $comment ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } $query['parent'] = $comment_id; } $comments = get_comments( $query ); if ( $args['hierarchical'] ) { $walker = new WPCOM_JSON_API_List_Comments_Walker(); $comment_ids = $walker->paged_walk( $comments, get_option( 'thread_comments_depth', -1 ), isset( $args['page'] ) ? $args['page'] : 1, $args['number'] ); if ( ! empty( $comment_ids ) ) { $comments = array_map( 'get_comment', $comment_ids ); } } $return = array(); foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $return[ $key ] = (int) $found; break; case 'site_ID': $return[ $key ] = (int) $blog_id; break; case 'comments': $return_comments = array(); if ( ! empty( $comments ) ) { foreach ( $comments as $comment ) { $the_comment = $this->get_comment( $comment->comment_ID, $args['context'] ); if ( $the_comment && ! is_wp_error( $the_comment ) ) { $return_comments[] = $the_comment; } } } if ( $return_comments ) { /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'comments', count( $return_comments ) ); } $return[ $key ] = $return_comments; break; } } return $return; } } class.wpcom-json-api-get-taxonomies-endpoint.php 0000644 00000012711 14722054026 0016007 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Taxonomies_Endpoint( array( 'description' => "Get a list of a site's categories.", 'group' => 'taxonomy', 'stat' => 'categories', 'method' => 'GET', 'path' => '/sites/%s/categories', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=100) The number of categories to return. Limit: 1000.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of categories. Takes precedence over the <code>offset</code> parameter.', 'search' => '(string) Limit response to include only categories whose names or slugs match the provided search query.', 'order' => array( 'ASC' => 'Return categories in ascending order.', 'DESC' => 'Return categories in descending order.', ), 'order_by' => array( 'name' => 'Order by the name of each category.', 'count' => 'Order by the number of posts in each category.', ), ), 'response_format' => array( 'found' => '(int) The number of categories returned.', 'categories' => '(array) Array of category objects.', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/categories/?number=5', ) ); new WPCOM_JSON_API_Get_Taxonomies_Endpoint( array( 'description' => "Get a list of a site's tags.", 'group' => 'taxonomy', 'stat' => 'tags', 'method' => 'GET', 'path' => '/sites/%s/tags', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=100) The number of tags to return. Limit: 1000.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of tags. Takes precedence over the <code>offset</code> parameter.', 'search' => '(string) Limit response to include only tags whose names or slugs match the provided search query.', 'order' => array( 'ASC' => 'Return tags in ascending order.', 'DESC' => 'Return tags in descending order.', ), 'order_by' => array( 'name' => 'Order by the name of each tag.', 'count' => 'Order by the number of posts in each tag.', ), ), 'allow_fallback_to_jetpack_blog_token' => true, 'response_format' => array( 'found' => '(int) The number of tags returned.', 'tags' => '(array) Array of tag objects.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/?number=5', ) ); /** * GET taxonomies endpoint class. */ class WPCOM_JSON_API_Get_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * /sites/%s/tags -> $blog_id * /sites/%s/categories -> $blog_id * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $args = $this->process_args( $args ); if ( preg_match( '#/tags#i', $path ) ) { return $this->tags( $args ); } else { return $this->categories( $args ); } } /** * Process args. * * @param array $args - the arguments. */ public function process_args( $args ) { if ( $args['number'] < 1 ) { $args['number'] = 100; } elseif ( 1000 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 1000.', 400 ); } if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $args['offset'] = ( $args['page'] - 1 ) * $args['number']; unset( $args['page'] ); } if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $args['orderby'] = $args['order_by']; unset( $args['order_by'] ); unset( $args['context'], $args['pretty'], $args['http_envelope'], $args['fields'] ); return $args; } /** * Get categories. * * @param array $args - the arguments. */ public function categories( $args ) { $args['get'] = 'all'; $cats = get_categories( $args ); unset( $args['offset'] ); $args['taxonomy'] = 'category'; $found = wp_count_terms( $args ); $cats_obj = array(); foreach ( $cats as $cat ) { $cats_obj[] = $this->format_taxonomy( $cat, 'category', 'display' ); } return array( 'found' => (int) $found, 'categories' => $cats_obj, ); } /** * Get tags. * * @param array $args - the arguments. */ public function tags( $args ) { $args['get'] = 'all'; $tags = (array) get_tags( $args ); unset( $args['offset'] ); $args['taxonomy'] = 'post_tag'; $found = wp_count_terms( $args ); $tags_obj = array(); foreach ( $tags as $tag ) { $tags_obj[] = $this->format_taxonomy( $tag, 'post_tag', 'display' ); } return array( 'found' => (int) $found, 'tags' => $tags_obj, ); } } class.wpcom-json-api-site-settings-endpoint.php 0000644 00000156504 14722054026 0015657 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Manage settings via the WordPress.com REST API. * * @package automattic/jetpack */ use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions; new WPCOM_JSON_API_Site_Settings_Endpoint( array( 'description' => 'Get detailed settings information about a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'max_version' => '1.1', 'new_version' => '1.2', 'method' => 'GET', 'path' => '/sites/%s/settings', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'context' => false, ), 'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings', ) ); new WPCOM_JSON_API_Site_Settings_Endpoint( array( 'description' => 'Update settings for a site.', 'group' => '__do_not_document', 'stat' => 'sites:X', 'max_version' => '1.1', 'new_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/settings', 'a_new_very_long_key' => 'blabla', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'migration_source_site_domain' => '(string) The source site URL, from the migration flow', 'in_site_migration_flow' => '(string) The migration flow the site is in', 'blogname' => '(string) Blog name', 'blogdescription' => '(string) Blog description', 'default_pingback_flag' => '(bool) Notify blogs linked from article?', 'default_ping_status' => '(bool) Allow link notifications from other blogs?', 'default_comment_status' => '(bool) Allow comments on new articles?', 'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines', 'wpcom_data_sharing_opt_out' => '(bool) Did the site opt out of sharing public content with third parties and research partners?', 'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati', 'jetpack_relatedposts_enabled' => '(bool) Enable related posts?', 'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?', 'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?', 'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?', 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?', 'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow', 'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface', 'jetpack_search_enabled' => '(bool) Enable Jetpack Search', 'jetpack_search_supported' => '(bool) Jetpack Search is supported', 'infinite_scroll' => '(bool) Support infinite scroll of posts?', 'default_category' => '(int) Default post category', 'default_post_format' => '(string) Default post format', 'require_name_email' => '(bool) Require comment authors to fill out name and email?', 'comment_registration' => '(bool) Require users to be registered and logged in to comment?', 'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?', 'close_comments_days_old' => '(int) Age at which to close comments', 'thread_comments' => '(bool) Enable threaded comments?', 'thread_comments_depth' => '(int) Depth to thread comments', 'page_comments' => '(bool) Break comments into pages?', 'comments_per_page' => '(int) Number of comments to display per page', 'default_comments_page' => '(string) newest|oldest Which page of comments to display first', 'comment_order' => '(string) asc|desc Order to display comments within page', 'comments_notify' => '(bool) Email me when someone comments?', 'moderation_notify' => '(bool) Email me when a comment is helf for moderation?', 'social_notifications_like' => '(bool) Email me when someone likes my post?', 'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?', 'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?', 'comment_moderation' => '(bool) Moderate comments for manual approval?', 'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?', 'comment_max_links' => '(int) Moderate comments that contain X or more links', 'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line', 'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line', 'lang_id' => '(int) ID for language blog is written in', 'wga' => '(array) Google Analytics Settings', 'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?', 'disabled_reblogs' => '(bool) Are reblogs disabled on posts?', 'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?', 'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)', 'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"', 'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed', 'sharing_open_links' => '(string) Link target for sharing buttons (same or new)', 'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button', 'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.', 'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site', 'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'', 'gmt_offset' => '(int) Site offset from UTC in hours', 'date_format' => '(string) PHP Date-compatible date format', 'time_format' => '(string) PHP Date-compatible time format', 'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)', 'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site', 'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page', 'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site', 'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page', Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The seo meta description for the site.', Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives', 'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook', 'markdown_supported' => '(bool) Whether markdown is supported for this site', 'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts', 'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments', 'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear', 'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache', 'posts_per_page' => '(int) Number of posts to show on blog pages', 'posts_per_rss' => '(int) Number of posts to show in the RSS feed', 'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts', 'launchpad_screen' => '(string) Whether or not launchpad is presented and what size it will be', 'sm_enabled' => '(bool) Whether the newsletter subscribe modal is enabled', 'jetpack_subscribe_overlay_enabled' => '(bool) Whether the newsletter subscribe overlay is enabled', 'jetpack_subscribe_floating_button_enabled' => '(bool) Whether the newsletter floating subscribe button is enabled', 'jetpack_subscriptions_subscribe_post_end_enabled' => '(bool) Whether the Subscribe block at the end of each post placement is enabled', 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled', 'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled', 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt', 'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules', 'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow', 'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled', 'jetpack_waf_ip_block_list' => '(string) List of IP addresses the WAF should always block', 'jetpack_waf_ip_block_list_enabled' => '(bool) Whether the IP block list is enabled', 'jetpack_waf_share_data' => '(bool) Whether the WAF should share basic data with Jetpack', 'jetpack_waf_share_debug_data' => '(bool) Whether the WAF should share debug data with Jetpack', 'jetpack_waf_automatic_rules_last_updated_timestamp' => '(int) Timestamp of the last time the automatic rules were updated', ), 'response_format' => array( 'updated' => '(array)', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings', ) ); /** * Manage Site settings endpoint. */ class WPCOM_JSON_API_Site_Settings_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Site format. * * @var array */ public static $site_format = array( 'ID' => '(int) Site ID', 'name' => '(string) Title of site', 'description' => '(string) Tagline or description of site', 'URL' => '(string) Full URL to the site', 'lang' => '(string) Primary language code of the site', 'locale_variant' => '(string) Locale variant code for the site, if set', 'settings' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site.', ); /** * Endpoint response * * GET /sites/%s/settings * POST /sites/%s/settings * * @param string $path Path. * @param int $blog_id Blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { // Source & include the infinite scroll compatibility files prior to loading theme functions. add_filter( 'restapi_theme_action_copy_dirs', array( 'WPCOM_JSON_API_Site_Settings_Endpoint', 'wpcom_restapi_copy_theme_plugin_actions' ) ); $this->load_theme_functions(); } if ( ! is_user_logged_in() ) { return new WP_Error( 'Unauthorized', 'You must be logged-in to manage settings.', 401 ); } elseif ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'Forbidden', 'You do not have the capability to manage settings for this site.', 403 ); } if ( 'GET' === $this->api->method ) { /** * Fires on each GET request to a specific endpoint. * * @module json-api * * @since 3.2.0 * * @param string sites. */ do_action( 'wpcom_json_api_objects', 'sites' ); return $this->get_settings_response(); } elseif ( 'POST' === $this->api->method ) { return $this->update_settings(); } else { return new WP_Error( 'bad_request', 'An unsupported request method was used.' ); } } /** * Includes additional theme-specific files to be included in REST API theme * context loading action copying. * * @see WPCOM_JSON_API_Endpoint#load_theme_functions * @see the_neverending_home_page_theme_support * * @param array $copy_dirs Array of files to be included in theme context. */ public static function wpcom_restapi_copy_theme_plugin_actions( $copy_dirs ) { $theme_name = get_stylesheet(); $default_file_name = WP_CONTENT_DIR . "/mu-plugins/infinity/themes/{$theme_name}.php"; /** * Filter the path to the Infinite Scroll compatibility file. * * @module infinite-scroll * * @since 2.0.0 * * @param string $str IS compatibility file path. * @param string $theme_name Theme name. */ $customization_file = apply_filters( 'infinite_scroll_customization_file', $default_file_name, $theme_name ); if ( is_readable( $customization_file ) ) { require_once $customization_file; $copy_dirs[] = $customization_file; } return $copy_dirs; } /** * Determines whether jetpack_relatedposts is supported * * @return bool */ public function jetpack_relatedposts_supported() { $wpcom_related_posts_theme_blacklist = array( 'Expound', 'Traveler', 'Opti', 'Currents', ); return ( ! in_array( wp_get_theme()->get( 'Name' ), $wpcom_related_posts_theme_blacklist, true ) ); } /** * Returns category details * * @param WP_Term $category Category object. * * @return array */ public function get_category_details( $category ) { return array( 'value' => $category->term_id, 'name' => $category->name, ); } /** * Returns an option value as the result of the callable being applied to * it if a value is set, otherwise null. * * @param string $option_name Option name. * @param callable $cast_callable Callable to invoke on option value. * * @return int|null Numeric option value or null. */ protected function get_cast_option_value_or_null( $option_name, $cast_callable ) { $option_value = get_option( $option_name, null ); if ( $option_value === null ) { return $option_value; } return call_user_func( $cast_callable, $option_value ); } /** * Collects the necessary information to return for a get settings response. * * @return array */ public function get_settings_response() { $response = array(); // Allow update in later versions. /** * Filter the structure of site settings to return. * * @module json-api * * @since 3.9.3 * * @param array $site_format Data structure. */ $response_format = apply_filters( 'site_settings_site_format', self::$site_format ); $blog_id = (int) $this->api->get_blog_id_for_output(); $site = $this->get_platform()->get_site( $blog_id ); foreach ( array_keys( $response_format ) as $key ) { // refactoring to change lang parameter to locale in 1.2. $lang_or_locale = $this->get_locale( $key ); if ( $lang_or_locale ) { $response[ $key ] = $lang_or_locale; continue; } switch ( $key ) { case 'ID': $response[ $key ] = $blog_id; break; case 'name': $response[ $key ] = (string) htmlspecialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); break; case 'description': $response[ $key ] = (string) htmlspecialchars_decode( get_bloginfo( 'description' ), ENT_QUOTES ); break; case 'URL': $response[ $key ] = (string) home_url(); break; case 'locale_variant': if ( function_exists( 'wpcom_l10n_get_blog_locale_variant' ) ) { $blog_locale_variant = wpcom_l10n_get_blog_locale_variant(); if ( $blog_locale_variant ) { $response[ $key ] = $blog_locale_variant; } } break; case 'settings': $jetpack_relatedposts_options = Jetpack_Options::get_option( 'relatedposts', array() ); // If the option's enabled key is NOT SET, it is considered enabled by the plugin. if ( ! isset( $jetpack_relatedposts_options['enabled'] ) ) { $jetpack_relatedposts_options['enabled'] = true; } $jetpack_relatedposts_options['enabled'] = $jetpack_relatedposts_options['enabled'] && $site->is_module_active( 'related-posts' ); $jetpack_search_supported = false; if ( function_exists( 'wpcom_is_jetpack_search_supported' ) ) { $jetpack_search_supported = wpcom_is_jetpack_search_supported( $blog_id ); } $jetpack_search_active = $jetpack_search_supported && $site->is_module_active( 'search' ); // array_values() is necessary to ensure the array starts at index 0. $post_categories = array_values( array_map( array( $this, 'get_category_details' ), get_categories( array( 'hide_empty' => false ) ) ) ); $newsletter_categories = maybe_unserialize( get_option( 'wpcom_newsletter_categories', array() ) ); $newsletter_category_ids = array_map( function ( $newsletter_category ) { return $newsletter_category['term_id']; }, $newsletter_categories ); $api_cache = $site->is_jetpack() ? (bool) get_option( 'jetpack_api_cache_enabled' ) : true; $response[ $key ] = array( // also exists as "options". 'admin_url' => get_admin_url(), 'default_ping_status' => 'closed' !== get_option( 'default_ping_status' ), 'default_comment_status' => 'closed' !== get_option( 'default_comment_status' ), // new stuff starts here. 'instant_search_enabled' => (bool) get_option( 'instant_search_enabled' ), 'blog_public' => (int) get_option( 'blog_public' ), 'wpcom_data_sharing_opt_out' => (bool) get_option( 'wpcom_data_sharing_opt_out' ), 'jetpack_sync_non_public_post_stati' => (bool) Jetpack_Options::get_option( 'sync_non_public_post_stati' ), 'jetpack_relatedposts_allowed' => (bool) $this->jetpack_relatedposts_supported(), 'jetpack_relatedposts_enabled' => (bool) $jetpack_relatedposts_options['enabled'], 'jetpack_relatedposts_show_context' => ! empty( $jetpack_relatedposts_options['show_context'] ), 'jetpack_relatedposts_show_date' => ! empty( $jetpack_relatedposts_options['show_date'] ), 'jetpack_relatedposts_show_headline' => ! empty( $jetpack_relatedposts_options['show_headline'] ), 'jetpack_relatedposts_show_thumbnails' => ! empty( $jetpack_relatedposts_options['show_thumbnails'] ), 'jetpack_search_enabled' => (bool) $jetpack_search_active, 'jetpack_search_supported' => (bool) $jetpack_search_supported, 'default_category' => (int) get_option( 'default_category' ), 'post_categories' => (array) $post_categories, 'default_post_format' => get_option( 'default_post_format' ), 'default_pingback_flag' => (bool) get_option( 'default_pingback_flag' ), 'require_name_email' => (bool) get_option( 'require_name_email' ), 'comment_registration' => (bool) get_option( 'comment_registration' ), 'close_comments_for_old_posts' => (bool) get_option( 'close_comments_for_old_posts' ), 'close_comments_days_old' => (int) get_option( 'close_comments_days_old' ), 'thread_comments' => (bool) get_option( 'thread_comments' ), 'thread_comments_depth' => (int) get_option( 'thread_comments_depth' ), 'page_comments' => (bool) get_option( 'page_comments' ), 'comments_per_page' => (int) get_option( 'comments_per_page' ), 'default_comments_page' => get_option( 'default_comments_page' ), 'comment_order' => get_option( 'comment_order' ), 'comments_notify' => (bool) get_option( 'comments_notify' ), 'moderation_notify' => (bool) get_option( 'moderation_notify' ), 'social_notifications_like' => ( 'on' === get_option( 'social_notifications_like' ) ), 'social_notifications_reblog' => ( 'on' === get_option( 'social_notifications_reblog' ) ), 'social_notifications_subscribe' => ( 'on' === get_option( 'social_notifications_subscribe' ) ), 'comment_moderation' => (bool) get_option( 'comment_moderation' ), 'comment_whitelist' => (bool) get_option( 'comment_previously_approved' ), 'comment_previously_approved' => (bool) get_option( 'comment_previously_approved' ), 'comment_max_links' => (int) get_option( 'comment_max_links' ), 'moderation_keys' => get_option( 'moderation_keys' ), 'blacklist_keys' => get_option( 'disallowed_keys' ), 'disallowed_keys' => get_option( 'disallowed_keys' ), 'lang_id' => defined( 'IS_WPCOM' ) && IS_WPCOM ? get_lang_id_by_code( wpcom_l10n_get_blog_locale_variant( $blog_id, true ) ) : get_option( 'lang_id' ), 'site_vertical_id' => (string) get_option( 'site_vertical_id' ), 'jetpack_cloudflare_analytics' => get_option( 'jetpack_cloudflare_analytics' ), 'disabled_likes' => (bool) get_option( 'disabled_likes' ), 'disabled_reblogs' => (bool) get_option( 'disabled_reblogs' ), 'jetpack_comment_likes_enabled' => (bool) get_option( 'jetpack_comment_likes_enabled', false ), 'twitter_via' => (string) get_option( 'twitter_via' ), 'jetpack-twitter-cards-site-tag' => (string) get_option( 'jetpack-twitter-cards-site-tag' ), 'eventbrite_api_token' => $this->get_cast_option_value_or_null( 'eventbrite_api_token', 'intval' ), 'gmt_offset' => get_option( 'gmt_offset' ), 'timezone_string' => get_option( 'timezone_string' ), 'date_format' => get_option( 'date_format' ), 'time_format' => get_option( 'time_format' ), 'start_of_week' => get_option( 'start_of_week' ), 'woocommerce_onboarding_profile' => (array) get_option( 'woocommerce_onboarding_profile', array() ), 'woocommerce_store_address' => (string) get_option( 'woocommerce_store_address' ), 'woocommerce_store_address_2' => (string) get_option( 'woocommerce_store_address_2' ), 'woocommerce_store_city' => (string) get_option( 'woocommerce_store_city' ), 'woocommerce_default_country' => (string) get_option( 'woocommerce_default_country' ), 'woocommerce_store_postcode' => (string) get_option( 'woocommerce_store_postcode' ), 'jetpack_testimonial' => (bool) get_option( 'jetpack_testimonial', '0' ), 'jetpack_testimonial_posts_per_page' => (int) get_option( 'jetpack_testimonial_posts_per_page', '10' ), 'jetpack_portfolio' => (bool) get_option( 'jetpack_portfolio', '0' ), 'jetpack_portfolio_posts_per_page' => (int) get_option( 'jetpack_portfolio_posts_per_page', '10' ), 'markdown_supported' => true, 'site_icon' => $this->get_cast_option_value_or_null( 'site_icon', 'intval' ), Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => get_option( Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION, '' ), Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => get_option( Jetpack_SEO_Titles::TITLE_FORMATS_OPTION, array() ), 'api_cache' => $api_cache, 'posts_per_page' => (int) get_option( 'posts_per_page' ), 'posts_per_rss' => (int) get_option( 'posts_per_rss' ), 'rss_use_excerpt' => (bool) get_option( 'rss_use_excerpt' ), 'launchpad_screen' => (string) get_option( 'launchpad_screen' ), 'wpcom_featured_image_in_email' => (bool) get_option( 'wpcom_featured_image_in_email' ), 'jetpack_gravatar_in_email' => (bool) get_option( 'jetpack_gravatar_in_email', true ), 'jetpack_author_in_email' => (bool) get_option( 'jetpack_author_in_email', true ), 'jetpack_post_date_in_email' => (bool) get_option( 'jetpack_post_date_in_email', true ), 'wpcom_newsletter_categories' => $newsletter_category_ids, 'wpcom_newsletter_categories_enabled' => (bool) get_option( 'wpcom_newsletter_categories_enabled' ), 'sm_enabled' => (bool) get_option( 'sm_enabled' ), 'jetpack_subscribe_overlay_enabled' => (bool) get_option( 'jetpack_subscribe_overlay_enabled' ), 'jetpack_subscribe_floating_button_enabled' => (bool) get_option( 'jetpack_subscribe_floating_button_enabled' ), 'jetpack_subscriptions_subscribe_post_end_enabled' => (bool) get_option( 'jetpack_subscriptions_subscribe_post_end_enabled' ), 'jetpack_subscriptions_login_navigation_enabled' => (bool) get_option( 'jetpack_subscriptions_login_navigation_enabled' ), 'jetpack_subscriptions_subscribe_navigation_enabled' => (bool) get_option( 'jetpack_subscriptions_subscribe_navigation_enabled' ), 'wpcom_gifting_subscription' => (bool) get_option( 'wpcom_gifting_subscription', $this->get_wpcom_gifting_subscription_default() ), 'wpcom_reader_views_enabled' => (bool) get_option( 'wpcom_reader_views_enabled', true ), 'wpcom_subscription_emails_use_excerpt' => (bool) get_option( 'wpcom_subscription_emails_use_excerpt' ), 'jetpack_subscriptions_reply_to' => (string) $this->get_subscriptions_reply_to_option(), 'jetpack_subscriptions_from_name' => (string) get_option( 'jetpack_subscriptions_from_name' ), 'show_on_front' => (string) get_option( 'show_on_front' ), 'page_on_front' => (string) get_option( 'page_on_front' ), 'page_for_posts' => (string) get_option( 'page_for_posts' ), 'subscription_options' => (array) get_option( 'subscription_options' ), 'jetpack_verbum_subscription_modal' => (bool) get_option( 'jetpack_verbum_subscription_modal', true ), 'enable_verbum_commenting' => (bool) get_option( 'enable_verbum_commenting', true ), 'enable_blocks_comments' => (bool) get_option( 'enable_blocks_comments', true ), 'highlander_comment_form_prompt' => $this->get_highlander_comment_form_prompt_option(), 'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ), 'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ), 'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ), 'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ), 'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ), 'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ), 'jetpack_waf_ip_block_list' => (string) get_option( 'jetpack_waf_ip_block_list' ), 'jetpack_waf_ip_block_list_enabled' => (bool) get_option( 'jetpack_waf_ip_block_list_enabled' ), 'jetpack_waf_share_data' => (bool) get_option( 'jetpack_waf_share_data' ), 'jetpack_waf_share_debug_data' => (bool) get_option( 'jetpack_waf_share_debug_data' ), 'jetpack_waf_automatic_rules_last_updated_timestamp' => (int) get_option( 'jetpack_waf_automatic_rules_last_updated_timestamp' ), 'is_fully_managed_agency_site' => (bool) get_option( 'is_fully_managed_agency_site' ), ); if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $response[ $key ]['wpcom_publish_posts_with_markdown'] = (bool) WPCom_Markdown::get_instance()->is_posting_enabled(); $response[ $key ]['wpcom_publish_comments_with_markdown'] = (bool) WPCom_Markdown::get_instance()->is_commenting_enabled(); // WPCOM-specific Infinite Scroll Settings. if ( is_callable( array( 'The_Neverending_Home_Page', 'get_settings' ) ) ) { /** * Clear the cached copy of widget info so it's pulled fresh from blog options. * It was primed during the initial load under the __REST API site__'s context. * * @see wp_get_sidebars_widgets https://core.trac.wordpress.org/browser/trunk/src/wp-includes/widgets.php?rev=42374#L931 */ $GLOBALS['_wp_sidebars_widgets'] = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $infinite_scroll_settings = The_Neverending_Home_Page::get_settings(); $response[ $key ]['infinite_scroll'] = get_option( 'infinite_scroll', true ) && 'scroll' === $infinite_scroll_settings->type; if ( $infinite_scroll_settings->footer_widgets || 'click' === $infinite_scroll_settings->requested_type ) { // The blog has footer widgets -- infinite scroll is blocked. $response[ $key ]['infinite_scroll_blocked'] = 'footer'; } else { $response[ $key ]['infinite_scroll_blocked'] = false; } } } // allow future versions of this endpoint to support additional settings keys. /** * Filter the current site setting in the returned response. * * @module json-api * * @since 3.9.3 * @since 13.6 Added the API object parameter. * * @param mixed $response_item A single site setting. * @param WPCOM_JSON_API_Site_Settings_Endpoint $this The API object. */ $response[ $key ] = apply_filters( 'site_settings_endpoint_get', $response[ $key ], $this ); if ( class_exists( 'Sharing_Service' ) ) { $ss = new Sharing_Service(); $sharing = $ss->get_global_options(); $response[ $key ]['sharing_button_style'] = (string) $sharing['button_style']; $response[ $key ]['sharing_label'] = (string) $sharing['sharing_label']; $response[ $key ]['sharing_show'] = (array) $sharing['show']; $response[ $key ]['sharing_open_links'] = (string) $sharing['open_links']; } $response[ $key ]['jetpack_protect_whitelist'] = Brute_Force_Protection_Shared_Functions::format_allow_list(); if ( ! current_user_can( 'edit_posts' ) ) { unset( $response[ $key ] ); } break; } } return $response; } /** * Get the default value for the wpcom_gifting_subscription option. * The default value is the inverse of the plan's auto_renew setting. * * @return bool */ protected function get_wpcom_gifting_subscription_default() { if ( function_exists( 'wpcom_get_site_purchases' ) && function_exists( 'wpcom_purchase_has_feature' ) ) { $purchases = wpcom_get_site_purchases(); foreach ( $purchases as $purchase ) { if ( wpcom_purchase_has_feature( $purchase, \WPCOM_Features::SUBSCRIPTION_GIFTING ) ) { /* * We set default value as false when expiration date not match the following: * - 54 days before the annual plan expiration. * - 5 days before the monthly plan expiration. * This is to match the gifting banner logic. */ $days_of_warning = str_contains( $purchase->product_slug, 'monthly' ) ? 5 : 54; $seconds_until_expiration = strtotime( $purchase->expiry_date ) - time(); if ( $seconds_until_expiration >= $days_of_warning * DAY_IN_SECONDS ) { return false; } // We set default to the inverse of auto-renew. if ( isset( $purchase->auto_renew ) ) { return ! $purchase->auto_renew; } elseif ( isset( $purchase->user_allows_auto_renew ) ) { return ! $purchase->user_allows_auto_renew; } } } } return false; } /** * Get locale. * * @param string $key Language. */ protected function get_locale( $key ) { if ( 'lang' === $key ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return (string) get_blog_lang_code(); } else { return get_locale(); } } return false; } /** * Updates site settings for authorized users * * @return array|WP_Error */ public function update_settings() { /* * $this->input() retrieves posted arguments whitelisted and casted to the $request_format * specs that get passed in when this class is instantiated */ $input = $this->input(); $unfiltered_input = $this->input( false, false ); /** * Filters the settings to be updated on the site. * * @module json-api * * @since 3.6.0 * @since 6.1.1 Added $unfiltered_input parameter. * * @param array $input Associative array of site settings to be updated. * Cast and filtered based on documentation. * @param array $unfiltered_input Associative array of site settings to be updated. * Neither cast nor filtered. Contains raw input. */ $input = apply_filters( 'rest_api_update_site_settings', $input, $unfiltered_input ); $blog_id = get_current_blog_id(); $jetpack_relatedposts_options = array(); $sharing_options = array(); $updated = array(); foreach ( $input as $key => $value ) { if ( ! is_array( $value ) ) { $value = trim( $value ); } // preserve the raw value before unslashing the value. The slashes need to be preserved for date and time formats. $raw_value = $value; $value = wp_unslash( $value ); switch ( $key ) { case 'default_ping_status': case 'default_comment_status': // settings are stored as closed|open. $coerce_value = ( $value ) ? 'open' : 'closed'; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = $value; } break; case 'launchpad_screen': if ( in_array( $value, array( 'full', 'off', 'minimized' ), true ) ) { if ( update_option( $key, $value ) ) { $updated[ $key ] = $value; } } break; case 'jetpack_protect_whitelist': if ( class_exists( 'Brute_Force_Protection_Shared_Functions' ) ) { $result = Brute_Force_Protection_Shared_Functions::save_allow_list( $value ); if ( is_wp_error( $result ) ) { return $result; } $updated[ $key ] = Brute_Force_Protection_Shared_Functions::format_allow_list(); } break; case 'jetpack_sync_non_public_post_stati': Jetpack_Options::update_option( 'sync_non_public_post_stati', $value ); break; case 'jetpack_search_enabled': if ( $value ) { Jetpack::activate_module( $blog_id, 'search' ); } else { Jetpack::deactivate_module( $blog_id, 'search' ); } $updated[ $key ] = (bool) $value; break; case 'jetpack_relatedposts_enabled': case 'jetpack_relatedposts_show_context': case 'jetpack_relatedposts_show_date': case 'jetpack_relatedposts_show_thumbnails': case 'jetpack_relatedposts_show_headline': if ( ! $this->jetpack_relatedposts_supported() ) { break; } if ( 'jetpack_relatedposts_enabled' === $key ) { if ( $value ) { Jetpack::activate_module( $blog_id, 'related-posts' ); } else { Jetpack::deactivate_module( $blog_id, 'related-posts' ); } } $just_the_key = substr( $key, 21 ); $jetpack_relatedposts_options[ $just_the_key ] = $value; break; case 'social_notifications_like': case 'social_notifications_reblog': case 'social_notifications_subscribe': // settings are stored as on|off. $coerce_value = ( $value ) ? 'on' : 'off'; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = $value; } break; case 'cloudflare_analytics': if ( ! isset( $value['code'] ) || ! preg_match( '/^$|^[a-fA-F0-9]+$/i', $value['code'] ) ) { return new WP_Error( 'invalid_code', __( 'Invalid Cloudflare Analytics ID', 'jetpack' ) ); } if ( update_option( $key, $value ) ) { $updated[ $key ] = $value; } break; case 'jetpack_testimonial': case 'jetpack_portfolio': case 'jetpack_comment_likes_enabled': case 'wpcom_reader_views_enabled': case 'jetpack_verbum_subscription_modal': // settings are stored as 1|0. $coerce_value = (int) $value; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = (bool) $value; } break; case 'jetpack_testimonial_posts_per_page': case 'jetpack_portfolio_posts_per_page': // settings are stored as numeric. $coerce_value = (int) $value; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = $coerce_value; } break; // Sharing options. case 'sharing_button_style': case 'sharing_show': case 'sharing_open_links': $sharing_options[ preg_replace( '/^sharing_/', '', $key ) ] = $value; break; case 'sharing_label': $sharing_options[ $key ] = $value; break; // Keyring token option. case 'eventbrite_api_token': // These options can only be updated for sites hosted on WordPress.com. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) { if ( delete_option( $key ) ) { $updated[ $key ] = null; } } elseif ( update_option( $key, $value ) ) { $updated[ $key ] = (int) $value; } } break; case 'api_cache': if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) { if ( delete_option( 'jetpack_api_cache_enabled' ) ) { $updated[ $key ] = false; } } elseif ( update_option( 'jetpack_api_cache_enabled', true ) ) { $updated[ $key ] = true; } break; case 'timezone_string': /* * Map UTC+- timezones to gmt_offsets and set timezone_string to empty * https://github.com/WordPress/WordPress/blob/4.4.2/wp-admin/options.php#L175 */ if ( ! empty( $value ) && preg_match( '/^UTC[+-]/', $value ) ) { $gmt_offset = preg_replace( '/UTC\+?/', '', $value ); if ( update_option( 'gmt_offset', $gmt_offset ) ) { $updated['gmt_offset'] = $gmt_offset; } $value = ''; } /* * Always set timezone_string either with the given value or with an * empty string */ if ( update_option( $key, $value ) ) { $updated[ $key ] = $value; } break; case 'subscription_options': if ( ! is_array( $value ) ) { break; } $allowed_keys = array( 'invitation', 'comment_follow', 'welcome' ); $filtered_value = array_filter( $value, function ( $key ) use ( $allowed_keys ) { return in_array( $key, $allowed_keys, true ); }, ARRAY_FILTER_USE_KEY ); if ( empty( $filtered_value ) ) { break; } array_walk_recursive( $filtered_value, function ( &$value ) { $value = wp_kses( $value, array( 'a' => array( 'href' => array(), ), ) ); } ); $old_subscription_options = get_option( 'subscription_options' ); $new_subscription_options = array_merge( $old_subscription_options, $filtered_value ); if ( update_option( $key, $new_subscription_options ) ) { $updated[ $key ] = $filtered_value; } break; case 'woocommerce_onboarding_profile': // Allow boolean values but sanitize_text_field everything else. $sanitized_value = (array) $value; array_walk_recursive( $sanitized_value, function ( &$value ) { if ( ! is_bool( $value ) ) { $value = sanitize_text_field( $value ); } } ); if ( update_option( $key, $sanitized_value ) ) { $updated[ $key ] = $sanitized_value; } break; case 'woocommerce_store_address': case 'woocommerce_store_address_2': case 'woocommerce_store_city': case 'woocommerce_default_country': case 'woocommerce_store_postcode': $sanitized_value = sanitize_text_field( $value ); if ( update_option( $key, $sanitized_value ) ) { $updated[ $key ] = $sanitized_value; } break; case 'date_format': case 'time_format': // settings are stored as strings. // raw_value is used to help preserve any escaped characters that might exist in the formatted string. $sanitized_value = sanitize_text_field( $raw_value ); if ( update_option( $key, $sanitized_value ) ) { $updated[ $key ] = $sanitized_value; } break; case 'start_of_week': // setting is stored as int in 0-6 range (days of week). $coerce_value = (int) $value; $limit_value = ( $coerce_value >= 0 && $coerce_value <= 6 ) ? $coerce_value : 0; if ( update_option( $key, $limit_value ) ) { $updated[ $key ] = $limit_value; } break; case 'site_icon': /* * settings are stored as deletable numeric (all empty * values as delete intent), validated as media image */ if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) { /** * Fallback mechanism to clear a third party site icon setting. Can be used * to unset the option when an API request instructs the site to remove the site icon. * * @module json-api * * @since 4.10 */ if ( delete_option( $key ) || apply_filters( 'rest_api_site_icon_cleared', false ) ) { $updated[ $key ] = null; } } elseif ( is_numeric( $value ) ) { $coerce_value = (int) $value; if ( wp_attachment_is_image( $coerce_value ) && update_option( $key, $coerce_value ) ) { $updated[ $key ] = $coerce_value; } } break; case Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION: if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() && ! Jetpack_SEO_Utils::has_legacy_front_page_meta() ) { return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 ); } if ( ! is_string( $value ) ) { return new WP_Error( 'invalid_input', __( 'Invalid SEO meta description value.', 'jetpack' ), 400 ); } $new_description = Jetpack_SEO_Utils::update_front_page_meta_description( $value ); if ( ! empty( $new_description ) ) { $updated[ $key ] = $new_description; } break; case Jetpack_SEO_Titles::TITLE_FORMATS_OPTION: if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { if ( Jetpack_SEO_Utils::has_legacy_front_page_meta() ) { break; } return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 ); } if ( ! Jetpack_SEO_Titles::are_valid_title_formats( $value ) ) { return new WP_Error( 'invalid_input', __( 'Invalid SEO title format.', 'jetpack' ), 400 ); } $new_title_formats = Jetpack_SEO_Titles::update_title_formats( $value ); if ( ! empty( $new_title_formats ) ) { $updated[ $key ] = $new_title_formats; } break; case 'verification_services_codes': $verification_codes = jetpack_verification_validate( $value ); if ( update_option( 'verification_services_codes', $verification_codes ) ) { $updated[ $key ] = $verification_codes; } break; case 'wpcom_publish_posts_with_markdown': case 'wpcom_publish_comments_with_markdown': $coerce_value = (bool) $value; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = $coerce_value; } break; case 'wpcom_gifting_subscription': $coerce_value = (bool) $value; /* * get_option returns a boolean false if the option doesn't exist, otherwise it always returns * a serialized value. Knowing that we can check if the option already exists. */ $gift_toggle = get_option( $key ); if ( false === $gift_toggle ) { // update_option will not create a new option if the initial value is false. So use add_option. if ( add_option( $key, $coerce_value ) ) { $updated[ $key ] = $coerce_value; } } elseif ( update_option( $key, $coerce_value ) ) { // If the option already exists use update_option. $updated[ $key ] = $coerce_value; } break; case 'rss_use_excerpt': $sanitized_value = (int) (bool) $value; update_option( $key, $sanitized_value ); $updated[ $key ] = $sanitized_value; break; case 'wpcom_subscription_emails_use_excerpt': update_option( 'wpcom_subscription_emails_use_excerpt', (bool) $value ); $updated[ $key ] = (bool) $value; break; case 'jetpack_subscriptions_reply_to': require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php'; $to_set_value = Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value ) ? (string) $value : Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to; if ( update_option( $key, $to_set_value ) ) { $updated[ $key ] = $to_set_value; } break; case 'jetpack_subscriptions_from_name': $sanitized_value = sanitize_text_field( $value ); if ( update_option( $key, $sanitized_value ) ) { $updated[ $key ] = $sanitized_value; } break; case 'instant_search_enabled': update_option( 'instant_search_enabled', (bool) $value ); $updated[ $key ] = (bool) $value; break; case 'lang_id': /* * Due to the fact that locale variants are set in a locale_variant option, * changing locale from variant to primary * would look like the same lang_id is being saved and update_option would return false, * even though the correct options would be set by pre_update_option_lang_id, * so we should always return lang_id as updated. */ update_option( 'lang_id', (int) $value ); $updated[ $key ] = (int) $value; break; case 'wpcom_featured_image_in_email': update_option( 'wpcom_featured_image_in_email', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'wpcom_newsletter_categories': $sanitized_category_ids = (array) $value; array_walk_recursive( $sanitized_category_ids, function ( &$value ) { if ( is_int( $value ) && $value > 0 ) { return; } $value = (int) $value; if ( $value <= 0 ) { $value = null; } } ); $sanitized_category_ids = array_unique( array_filter( $sanitized_category_ids, function ( $category_id ) { return $category_id !== null; } ) ); $new_value = array_map( function ( $category_id ) { return array( 'term_id' => $category_id ); }, $sanitized_category_ids ); if ( update_option( $key, $new_value ) ) { $updated[ $key ] = $sanitized_category_ids; } break; case 'wpcom_newsletter_categories_enabled': update_option( 'wpcom_newsletter_categories_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'sm_enabled': update_option( 'sm_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'jetpack_subscribe_overlay_enabled': update_option( 'jetpack_subscribe_overlay_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'jetpack_subscribe_floating_button_enabled': update_option( 'jetpack_subscribe_floating_button_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'jetpack_subscriptions_subscribe_post_end_enabled': update_option( 'jetpack_subscriptions_subscribe_post_end_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'jetpack_subscriptions_login_navigation_enabled': update_option( 'jetpack_subscriptions_login_navigation_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'jetpack_subscriptions_subscribe_navigation_enabled': update_option( 'jetpack_subscriptions_subscribe_navigation_enabled', (int) (bool) $value ); $updated[ $key ] = (int) (bool) $value; break; case 'show_on_front': if ( in_array( $value, array( 'page', 'posts' ), true ) && update_option( $key, $value ) ) { $updated[ $key ] = $value; } break; case 'page_on_front': case 'page_for_posts': if ( $value === '' ) { // empty function is not applicable here because '0' may be a valid page id if ( delete_option( $key ) ) { $updated[ $key ] = null; } break; } if ( ! $this->is_valid_page_id( $value ) ) { break; } $related_option_key = $key === 'page_on_front' ? 'page_for_posts' : 'page_on_front'; $related_option_value = get_option( $related_option_key ); if ( $related_option_value === $value ) { // page_on_front and page_for_posts are not allowed to be the same break; } if ( update_option( $key, $value ) ) { $updated[ $key ] = $value; } break; case 'in_site_migration_flow': if ( empty( $value ) ) { delete_option( 'in_site_migration_flow' ); break; } $migration_flow_whitelist = array( 'site-migration', 'migration-signup', ); if ( ! in_array( $value, $migration_flow_whitelist, true ) ) { break; } update_option( 'in_site_migration_flow', $value ); $updated[ $key ] = $value; break; case 'migration_source_site_domain': // If we get an empty value, delete the option if ( empty( $value ) ) { delete_option( 'migration_source_site_domain' ); break; } // If we get a non-url value, don't update the option. if ( wp_http_validate_url( $value ) === false ) { break; } update_option( 'migration_source_site_domain', $value ); $updated[ $key ] = $value; break; case 'is_fully_managed_agency_site': $coerce_value = (int) (bool) $value; if ( update_option( $key, $coerce_value ) ) { $updated[ $key ] = (bool) $coerce_value; } break; default: // allow future versions of this endpoint to support additional settings keys. if ( has_filter( 'site_settings_endpoint_update_' . $key ) ) { /** * Filter current site setting value to be updated. * * @module json-api * * @since 3.9.3 * @since 13.6 Added the API object parameter. * * @param mixed $response_item A single site setting value. * @param WPCOM_JSON_API_Site_Settings_Endpoint The API object parameter. */ $value = apply_filters( 'site_settings_endpoint_update_' . $key, $value, $this ); if ( is_wp_error( $value ) ) { return $value; } if ( $value ) { $updated[ $key ] = $value; } break; } // no worries, we've already whitelisted and casted arguments above. if ( update_option( $key, $value ) ) { $updated[ $key ] = $value; } } } if ( $jetpack_relatedposts_options !== array() ) { // track new jetpack_relatedposts options against old. $old_relatedposts_options = Jetpack_Options::get_option( 'relatedposts' ); $jetpack_relatedposts_options_to_save = $old_relatedposts_options; foreach ( $jetpack_relatedposts_options as $key => $value ) { $jetpack_relatedposts_options_to_save[ $key ] = $value; } if ( Jetpack_Options::update_option( 'relatedposts', $jetpack_relatedposts_options_to_save ) ) { foreach ( $jetpack_relatedposts_options as $key => $value ) { if ( in_array( $key, array( 'show_context', 'show_date' ), true ) ) { $has_initialized_option = ! isset( $old_relatedposts_options[ $key ] ) && $value; $has_updated_option = isset( $old_relatedposts_options[ $key ] ) && $value !== $old_relatedposts_options[ $key ]; if ( $has_initialized_option || $has_updated_option ) { $updated[ 'jetpack_relatedposts_' . $key ] = (bool) $value; } } elseif ( isset( $old_relatedposts_options[ $key ] ) && $value !== $old_relatedposts_options[ $key ] ) { $updated[ 'jetpack_relatedposts_' . $key ] = $value; } } } } if ( ! empty( $sharing_options ) && class_exists( 'Sharing_Service' ) ) { $ss = new Sharing_Service(); /* * Merge current values with updated, since Sharing_Service expects * all values to be included when updating */ $current_sharing_options = $ss->get_global_options(); foreach ( $current_sharing_options as $key => $val ) { if ( ! isset( $sharing_options[ $key ] ) ) { $sharing_options[ $key ] = $val; } } $updated_social_options = $ss->set_global_options( $sharing_options ); if ( isset( $input['sharing_button_style'] ) ) { $updated['sharing_button_style'] = (string) $updated_social_options['button_style']; } if ( isset( $input['sharing_label'] ) ) { // Sharing_Service won't report label as updated if set to default. $updated['sharing_label'] = (string) $sharing_options['sharing_label']; } if ( isset( $input['sharing_show'] ) ) { $updated['sharing_show'] = (array) $updated_social_options['show']; } if ( isset( $input['sharing_open_links'] ) ) { $updated['sharing_open_links'] = (string) $updated_social_options['open_links']; } } return array( 'updated' => $updated, ); } /** * Get the string value of the jetpack_subscriptions_reply_to option. * When the option is not set, it will retun 'no-reply'. * * @return string */ protected function get_subscriptions_reply_to_option() { $reply_to = get_option( 'jetpack_subscriptions_reply_to', null ); if ( $reply_to === null ) { require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php'; return Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to; } return $reply_to; } /** * Check if the given value is a valid page ID for the current site. * * @param mixed $value The value to check. * @return bool True if the value is a valid page ID for the current site, false otherwise. */ protected function is_valid_page_id( $value ) { $all_page_ids = get_all_page_ids(); $valid_page_id = false; foreach ( $all_page_ids as $page_id ) { if ( $page_id === (string) $value ) { $valid_page_id = true; break; } } return $valid_page_id; } /** * Get the value of the highlander_comment_form_prompt option. * When the option is not set, it will return the default value. * * @return string */ protected function get_highlander_comment_form_prompt_option() { $highlander_comment_form_prompt_option = get_option( 'highlander_comment_form_prompt' ); if ( empty( $highlander_comment_form_prompt_option ) ) { return (string) __( 'Leave a comment', 'jetpack' ); } return (string) $highlander_comment_form_prompt_option; } } class.wpcom-json-api-bulk-restore-post-endpoint.php 0000644 00000004451 14722054026 0016447 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoint: /sites/%s/posts/restore */ new WPCOM_JSON_API_Bulk_Restore_Post_Endpoint( array( 'description' => 'Restore multiple posts.', 'group' => 'posts', 'stat' => 'posts:1:bulk-restore', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/restore', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'post_ids' => '(array|string) An array, or comma-separated list, of Post IDs to restore.', ), 'response_format' => array( 'results' => '(object) An object containing results, ', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/restore', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'post_ids' => array( 881, 882 ), ), ), ) ); /** * Bulk restore post endpoint class. */ class WPCOM_JSON_API_Bulk_Restore_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param object $object - parameter is for making the method signature compatible with its parent class method. */ public function callback( $path = '', $blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $input = $this->input(); if ( is_array( $input['post_ids'] ) ) { $post_ids = (array) $input['post_ids']; } elseif ( ! empty( $input['post_ids'] ) ) { $post_ids = explode( ',', $input['post_ids'] ); } else { $post_ids = array(); } if ( count( $post_ids ) < 1 ) { return new WP_Error( 'empty_post_ids', 'The request must include post_ids' ); } $result = array( 'results' => array(), ); foreach ( $post_ids as $post_id ) { $result['results'][ $post_id ] = $this->restore_post( $path, $blog_id, $post_id ); } return $result; } } class.wpcom-json-api-upload-media-v1-1-endpoint.php 0000644 00000027665 14722054026 0016105 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Upload media item API endpoint v1.1 * * Endpoint: /sites/%s/media/new */ new WPCOM_JSON_API_Upload_Media_v1_1_Endpoint( array( 'description' => 'Upload a new piece of media.', 'allow_cross_origin_request' => true, 'allow_upload_token_auth' => true, 'group' => 'media', 'stat' => 'media:new', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/media/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'media' => '(media) An array of media to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint.<br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", 'media_urls' => '(array) An array of URLs to upload to the post. Errors produced by media uploads, if any, will be in `media_errors` in the response.', 'attrs' => '(array) An array of attributes (`title`, `description`, `caption` `alt` for images, `artist` for audio, `album` for audio, and `parent_id`) are supported to assign to the media uploaded via the `media` or `media_urls` properties. You must use a numeric index for the keys of `attrs` which follows the same sequence as `media` and `media_urls`. <br /><br /><strong>Example</strong>:<br />' . "<code>curl \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://example.com/file2.jpg' \<br /> \<br />--form 'attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", ), 'response_format' => array( 'media' => '(array) Array of uploaded media objects', 'errors' => '(array) Array of error messages of uploading media failures', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/new', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'media_urls' => 'https://s.w.org/about/images/logos/codeispoetry-rgb.png', ), ), ) ); // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Upload media item API class v1.1 */ class WPCOM_JSON_API_Upload_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Upload media item API endpoint callback v1.1 * * @param string $path API path. * @param int $blog_id Blog ID. * * @return array|int|WP_Error|void */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'upload_files' ) && ! $this->api->is_authorized_with_upload_token() ) { return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 ); } $input = $this->input( true ); $media_files = ! empty( $input['media'] ) ? $input['media'] : array(); $media_urls = ! empty( $input['media_urls'] ) ? $input['media_urls'] : array(); $media_attrs = ! empty( $input['attrs'] ) ? $input['attrs'] : array(); if ( empty( $media_files ) && empty( $media_urls ) ) { return new WP_Error( 'invalid_input', 'No media provided in input.' ); } $jetpack_sync = null; $is_jetpack_site = false; if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { // For jetpack sites, we send the media via a different method, because the sync is very different. $jetpack_sync = Jetpack_Media_Sync::summon( $blog_id ); $is_jetpack_site = $jetpack_sync->is_jetpack_site(); } $jetpack_media_files = array(); $other_media_files = array(); $media_items = array(); $errors = array(); // We're splitting out videos for Jetpack sites. foreach ( $media_files as $media_item ) { if ( isset( $media_item['type'] ) && preg_match( '@^video/@', $media_item['type'] ) && $is_jetpack_site ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM && defined( 'VIDEOPRESS_JETPACK_VIDEO_ENABLED' ) && VIDEOPRESS_JETPACK_VIDEO_ENABLED ) { // Check that video upload space is available for a Jetpack site (skipped if site is Atomic). $result = videopress_check_space_available_for_jetpack( $blog_id, $media_item['name'], $media_item['size'] ); if ( true !== $result ) { $this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( array( $result ) ) ) ); continue; } } $jetpack_media_files[] = $media_item; } else { $other_media_files[] = $media_item; } } // New Jetpack / VideoPress media upload processing. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( is_countable( $jetpack_media_files ) && count( $jetpack_media_files ) > 0 ) { add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) ); // get_space_used() checks blog upload directory storage, // so filter it temporarily to return only video storage used. add_filter( 'pre_get_space_used', 'videopress_filter_jetpack_get_space_used' ); $media_items = $jetpack_sync->upload_media( $jetpack_media_files, $this->api ); $errors = $jetpack_sync->get_errors(); foreach ( $media_items as & $media_item ) { // More than likely a post has not been created yet, so we pass in the media item we // got back from the Jetpack site. $post = (object) $media_item['post']; $media_item = $this->get_media_item_v1_1( $post->ID, $post, $media_item['file'] ); } // Remove get_space_used filter after upload. remove_filter( 'pre_get_space_used', 'videopress_filter_jetpack_get_space_used' ); } } // Normal WPCOM upload processing. if ( ( is_countable( $other_media_files ) && count( $other_media_files ) > 0 ) || ( is_countable( $other_media_files ) && count( $media_urls ) > 0 ) ) { if ( is_multisite() ) { // Do not check for available space in non multisites. add_filter( 'wp_handle_upload_prefilter', array( $this, 'check_upload_size' ), 9 ); // used for direct media uploads. add_filter( 'wp_handle_sideload_prefilter', array( $this, 'check_upload_size' ), 9 ); // used for uploading media via url. } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { require_lib( 'tos-acceptance-tracking' ); add_filter( 'wp_handle_upload_prefilter', '\\A8C\\TOS_Acceptance_Tracking\\handle_uploads_wpcomtos_blog' ); } $create_media = $this->handle_media_creation_v1_1( $other_media_files, $media_urls, $media_attrs ); $media_ids = $create_media['media_ids']; $errors = $create_media['errors']; $media_items = array(); foreach ( $media_ids as $media_id ) { $media_items[] = $this->get_media_item_v1_1( $media_id ); } } if ( array() === $media_items ) { return $this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( $errors ) ) ); } $results = array(); foreach ( $media_items as $media_item ) { if ( is_wp_error( $media_item ) ) { $errors[] = array( 'file' => $media_item['ID'], 'error' => $media_item->get_error_code(), 'message' => $media_item->get_error_message(), ); } else { $results[] = $media_item; } } $response = array( 'media' => $results ); if ( is_countable( $errors ) && count( $errors ) > 0 ) { $response['errors'] = $this->rewrite_generic_upload_error( $errors ); } return $response; } /** * This changes the generic "upload_error" code to something more meaningful if possible * * @param array $errors Errors for the uploaded file. * @return array The same array with an improved error message. */ public function rewrite_generic_upload_error( $errors ) { foreach ( $errors as $k => $error ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( 'upload_error' === $error['error'] && str_contains( $error['message'], '|' ) ) { list( $errors[ $k ]['error'], $errors[ $k ]['message'] ) = explode( '|', $error['message'], 2 ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable } } return $errors; } /** * Determine if uploaded file exceeds space quota on multisite. * * This is a copy of the core function with added functionality, synced * with this with WP_REST_Attachments_Controller::check_upload_size() * to allow for specifying a better error message. * * @param array $file $_FILES array for a given file. * @return array Maybe extended with an error message. */ public function check_upload_size( $file ) { if ( get_site_option( 'upload_space_check_disabled' ) ) { return $file; } if ( isset( $file['error'] ) && $file['error'] > 0 ) { // There's already an error. Error Codes Reference: https://www.php.net/manual/en/features.file-upload.errors.php . return $file; } // We don't know if this is an upload or a sideload, but in either case the tmp_name should be a path, not a URL. if ( wp_parse_url( $file['tmp_name'], PHP_URL_SCHEME ) !== null ) { $file['error'] = 'rest_upload_invalid|' . __( 'Specified file failed upload test.', 'default' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch return $file; } if ( defined( 'WP_IMPORTING' ) ) { return $file; } $space_left = get_upload_space_available(); $file_size = filesize( $file['tmp_name'] ); if ( $space_left < $file_size ) { /* translators: %s: Required disk space in kilobytes. */ $file['error'] = 'rest_upload_limited_space|' . sprintf( __( 'Not enough space to upload. %s KB needed.', 'default' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch } $max_upload_size = KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ); if ( defined( 'IS_WPCOM' ) && IS_WPCOM && defined( 'WPCOM_MAX_UPLOAD_FILE_SIZE' ) ) { $max_upload_size = WPCOM_MAX_UPLOAD_FILE_SIZE; } if ( $file_size > $max_upload_size ) { /* translators: %s: Maximum allowed file size in kilobytes. */ $file['error'] = 'rest_upload_file_too_big|' . sprintf( __( 'This file is too big. Files must be less than %s KB in size.', 'jetpack' ), $max_upload_size / KB_IN_BYTES ); } if ( upload_is_user_over_quota( false ) ) { $file['error'] = 'rest_upload_user_quota_exceeded|' . __( 'You have used your space quota. Please delete files before uploading.', 'default' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch } return $file; } /** * Force to use the WPCOM API instead of proxy back to the Jetpack API if the blog is a paid Jetpack * blog w/ the VideoPress module enabled AND the uploaded file is a video. * * @param int $blog_id Blog ID. * @return bool */ public function force_wpcom_request( $blog_id ) { // We don't need to do anything if VideoPress is not enabled for the blog. if ( ! is_videopress_enabled_on_jetpack_blog( $blog_id ) ) { return false; } // Check to see if the upload is not a video type, if not then return false. $input = $this->input( true ); $media_files = ! empty( $input['media'] ) ? $input['media'] : array(); if ( empty( $media_files ) ) { return false; } foreach ( $media_files as $media_item ) { if ( ! isset( $media_item['type'] ) || ! preg_match( '@^video/@', $media_item['type'] ) ) { return false; } } // The API request should be for a blog w/ Jetpack, A valid plan, has VideoPress enabled, // and is a video file. Let's let it through. return true; } } jetpack/class.jetpack-json-api-plugins-new-endpoint.php 0000644 00000012072 14722054026 0017231 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; use Automattic\Jetpack\Automatic_Install_Skin; // POST /sites/%s/plugins/new new Jetpack_JSON_API_Plugins_New_Endpoint( array( 'description' => 'Install a plugin to a Jetpack site by uploading a zip file', 'group' => '__do_not_document', 'stat' => 'plugins:new', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'zip' => '(array) Reference to an uploaded plugin package zip file.', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/new', ) ); new Jetpack_JSON_API_Plugins_New_Endpoint( array( 'description' => 'Install a plugin to a Jetpack site by uploading a zip file', 'group' => '__do_not_document', 'stat' => 'plugins:new', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'zip' => '(array) Reference to an uploaded plugin package zip file.', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/new', ) ); /** * Plugins new endpoint class. * * POST /sites/%s/plugins/new */ class Jetpack_JSON_API_Plugins_New_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'install_plugins'; /** * The action. * * @var string */ protected $action = 'install'; /** * Validate call. * * @param int $_blog_id - the blog ID. * @param string $capability - the capability. * @param bool $check_manage_active - check if manage is active. * * @return bool|WP_Error a WP_Error object or true if things are good. */ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { $validate = parent::validate_call( $_blog_id, $capability, $check_manage_active ); if ( is_wp_error( $validate ) ) { // Lets delete the attachment... if the user doesn't have the right permissions to do things. $args = $this->input(); if ( isset( $args['zip'][0]['id'] ) ) { wp_delete_attachment( $args['zip'][0]['id'], true ); } } return $validate; } /** * No need to try to validate the plugin since we didn't pass one in. * * @param string $plugin - the plugin we're validating. */ protected function validate_input( $plugin ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->bulk = false; $this->plugins = array(); } /** * Install the plugin. * * @return bool|WP_Error */ public function install() { $args = $this->input(); if ( isset( $args['zip'][0]['id'] ) ) { $plugin_attachment_id = $args['zip'][0]['id']; $local_file = get_attached_file( $plugin_attachment_id ); if ( ! $local_file ) { return new WP_Error( 'local-file-does-not-exist' ); } $skin = new Automatic_Install_Skin(); $upgrader = new Plugin_Upgrader( $skin ); $pre_install_plugin_list = get_plugins(); $result = $upgrader->install( $local_file ); // clean up. wp_delete_attachment( $plugin_attachment_id, true ); if ( is_wp_error( $result ) ) { return $result; } $after_install_plugin_list = get_plugins(); $plugin = array_values( array_diff( array_keys( $after_install_plugin_list ), array_keys( $pre_install_plugin_list ) ) ); if ( ! $result ) { $error_code = $skin->get_main_error_code(); $message = $skin->get_main_error_message(); if ( empty( $message ) ) { $message = __( 'An unknown error occurred during installation', 'jetpack' ); } if ( 'download_failed' === $error_code ) { $error_code = 'no_package'; } return new WP_Error( $error_code, $message, 400 ); } if ( empty( $plugin ) ) { return new WP_Error( 'plugin_already_installed' ); } $this->plugins = $plugin; $this->log[ $plugin[0] ] = $upgrader->skin->get_upgrade_messages(); return true; } return new WP_Error( 'no_plugin_installed' ); } } jetpack/json-api-jetpack-endpoints.php 0000644 00000170051 14722054026 0014044 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName $json_jetpack_endpoints_dir = __DIR__ . '/'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-endpoint.php'; // THEMES require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-active-endpoint.php'; new Jetpack_JSON_API_Themes_Active_Endpoint( array( 'description' => 'Get the active theme of your blog', 'stat' => 'themes:mine', 'method' => 'GET', 'path' => '/sites/%s/themes/mine', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/mine', ) ); new Jetpack_JSON_API_Themes_Active_Endpoint( array( 'description' => 'Change the active theme of your blog', 'method' => 'POST', 'path' => '/sites/%s/themes/mine', 'stat' => 'themes:mine', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'context' => false, ), 'request_format' => array( 'theme' => '(string) The ID of the theme that should be activated', 'dont_change_homepage' => '(bool) Whether the homepage of the site should be replaced with the theme homepage', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'theme' => 'twentytwelve', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/mine', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-list-endpoint.php'; new Jetpack_JSON_API_Themes_List_Endpoint( array( 'description' => 'Get WordPress.com Themes allowed on your blog', 'group' => '__do_not_document', 'stat' => 'themes', 'method' => 'GET', 'path' => '/sites/%s/themes', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'found' => '(int) The total number of themes found.', 'themes' => '(array) An array of theme objects.', ), 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-get-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-new-endpoint.php'; // POST /sites/%s/themes/%new new Jetpack_JSON_API_Themes_New_Endpoint( array( 'description' => 'Install a theme to your jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:new', 'method' => 'POST', 'path' => '/sites/%s/themes/new', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'zip' => '(array) Reference to an uploaded theme package zip file.', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/new', ) ); new Jetpack_JSON_API_Themes_Get_Endpoint( array( 'description' => 'Get a single theme on a jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:get:1', 'method' => 'GET', 'path' => '/sites/%s/themes/%s', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$theme' => '(string) The theme slug', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/twentyfourteen', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-modify-endpoint.php'; new Jetpack_JSON_API_Themes_Modify_Endpoint( array( 'description' => 'Modify a single theme on a jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:modify:1', 'method' => 'POST', 'path' => '/sites/%s/themes/%s', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$theme' => '(string) The theme slug', ), 'request_format' => array( 'action' => '(string) Only possible value is \'update\'. More to follow.', 'autoupdate' => '(bool) Whether or not to automatically update the theme.', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'action' => 'update', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/twentyfourteen', ) ); new Jetpack_JSON_API_Themes_Modify_Endpoint( array( 'description' => 'Modify a list of themes on a jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:modify', 'method' => 'POST', 'path' => '/sites/%s/themes', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'action' => '(string) Only possible value is \'update\'. More to follow.', 'autoupdate' => '(bool) Whether or not to automatically update the theme.', 'themes' => '(array) A list of theme slugs', ), 'response_format' => array( 'themes' => '(array:theme) A list of theme objects', ), 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'action' => 'autoupdate_on', 'themes' => array( 'twentytwelve', 'twentyfourteen', ), ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-install-endpoint.php'; // POST /sites/%s/themes/%s/install new Jetpack_JSON_API_Themes_Install_Endpoint( array( 'description' => 'Install a theme to your jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:1:install', 'method' => 'POST', 'path' => '/sites/%s/themes/%s/install', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$theme' => '(int|string) The theme slug to install', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/twentyfourteen/install', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-themes-delete-endpoint.php'; // POST /sites/%s/themes/%s/delete new Jetpack_JSON_API_Themes_Delete_Endpoint( array( 'description' => 'Delete/Uninstall a theme from your jetpack blog', 'group' => '__do_not_document', 'stat' => 'themes:1:delete', 'method' => 'POST', 'path' => '/sites/%s/themes/%s/delete', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$theme' => '(string) The slug of the theme to delete', ), 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/twentyfourteen/delete', ) ); // PLUGINS require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-get-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-list-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-new-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-install-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-delete-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-modify-endpoint.php'; // PLUGINS V1.2 require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-plugins-modify-v1-2-endpoint.php'; // Jetpack Modules require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-modules-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-modules-get-endpoint.php'; new Jetpack_JSON_API_Modules_Get_Endpoint( array( 'description' => 'Get the info about a Jetpack Module on your Jetpack Site', 'method' => 'GET', 'path' => '/sites/%s/jetpack/modules/%s/', 'stat' => 'jetpack:modules:1', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$module' => '(string) The module name', ), 'response_format' => Jetpack_JSON_API_Modules_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/jetpack/modules/stats', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-modules-modify-endpoint.php'; new Jetpack_JSON_API_Modules_Modify_Endpoint( array( 'description' => 'Modify the status of a Jetpack Module on your Jetpack Site', 'method' => 'POST', 'path' => '/sites/%s/jetpack/modules/%s/', 'stat' => 'jetpack:modules:1', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$module' => '(string) The module name', ), 'request_format' => array( 'active' => '(bool) The module activation status', ), 'response_format' => Jetpack_JSON_API_Modules_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'active' => true, ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/jetpack/modules/stats', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-modules-list-endpoint.php'; new Jetpack_JSON_API_Modules_List_Endpoint( array( 'description' => 'Get the list of available Jetpack modules on your site', 'method' => 'GET', 'path' => '/sites/%s/jetpack/modules', 'stat' => 'jetpack:modules', 'min_version' => '1', 'max_version' => '1.1', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'found' => '(int) The total number of modules found.', 'modules' => '(array) An array of module objects.', ), 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/jetpack/modules', ) ); require_once $json_jetpack_endpoints_dir . 'class-jetpack-json-api-modules-list-v1-2-endpoint.php'; new Jetpack_JSON_API_Modules_List_V1_2_Endpoint( array( 'description' => 'Get the list of available Jetpack modules on your site', 'method' => 'GET', 'path' => '/sites/%s/jetpack/modules', 'stat' => 'jetpack:modules', 'min_version' => '1.2', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'modules' => '(array) An array of module objects.', ), 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/jetpack/modules', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-updates-status-endpoint.php'; new Jetpack_JSON_API_Updates_Status( array( 'description' => 'Get counts for available updates', 'method' => 'GET', 'path' => '/sites/%s/updates', 'stat' => 'updates', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'plugins' => '(int) The total number of plugins updates.', 'themes' => '(int) The total number of themes updates.', 'wordpress' => '(int) The total number of core updates.', 'translations' => '(int) The total number of translation updates.', 'total' => '(int) The total number of updates.', 'wp_version' => '(safehtml) The wp_version string.', 'wp_update_version' => '(safehtml) The wp_version to update string.', 'jp_version' => '(safehtml) The site Jetpack version.', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/updates', ) ); // Jetpack Extras require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-check-capabilities-endpoint.php'; new Jetpack_JSON_API_Check_Capabilities_Endpoint( array( 'description' => 'Check if the current user has a certain capability over a Jetpack site', 'method' => 'GET', 'path' => '/sites/%s/me/capability', 'stat' => 'me:capabulity', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => '(bool) True if the user has the queried capability.', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'capability' => 'A single capability or an array of capabilities', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/me/capability', ) ); // CORE require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-core-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-core-modify-endpoint.php'; new Jetpack_JSON_API_Core_Endpoint( array( 'description' => 'Gets info about a Jetpack blog\'s core installation', 'method' => 'GET', 'path' => '/sites/%s/core', 'stat' => 'core', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'version' => '(string) The current version', 'autoupdate' => '(bool) Whether or not we automatically update core', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/core', ) ); new Jetpack_JSON_API_Core_Modify_Endpoint( array( 'description' => 'Update WordPress installation on a Jetpack blog', 'method' => 'POST', 'path' => '/sites/%s/core/update', 'stat' => 'core:update', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'version' => '(string) The core version to update', ), 'response_format' => array( 'version' => '(string) The core version after the upgrade has run.', 'log' => '(array:safehtml) An array of log strings.', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/core/update', ) ); new Jetpack_JSON_API_Core_Endpoint( array( 'description' => 'Toggle automatic core updates for a Jetpack blog', 'method' => 'POST', 'path' => '/sites/%s/core', 'stat' => 'core', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'autoupdate' => '(bool) Whether or not we automatically update core', ), 'response_format' => array( 'version' => '(string) The current version', 'autoupdate' => '(bool) Whether or not we automatically update core', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'autoupdate' => true, ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/core', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-sync-endpoint.php'; // POST /sites/%s/sync new Jetpack_JSON_API_Sync_Endpoint( array( 'description' => 'Force sync of all options and constants', 'method' => 'POST', 'path' => '/sites/%s/sync', 'stat' => 'sync', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'modules' => '(string) Comma-delimited set of sync modules to use (default: all of them)', 'posts' => '(string) Comma-delimited list of post IDs to sync', 'comments' => '(string) Comma-delimited list of comment IDs to sync', 'users' => '(string) Comma-delimited list of user IDs to sync', ), 'response_format' => array( 'scheduled' => '(bool) Whether or not the synchronisation was started', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync', ) ); // GET /sites/%s/sync/status new Jetpack_JSON_API_Sync_Status_Endpoint( array( 'description' => 'Status of the current full sync or the previous full sync', 'method' => 'GET', 'path' => '/sites/%s/sync/status', 'stat' => 'sync-status', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'fields' => '(string|null) List of comma-separated fields to return (see `response_format`).', ), 'response_format' => array( 'posts_checksum' => '(string|null) Posts checksum. Needs to be requested using the filter parameter.', 'comments_checksum' => '(string|null) Comments checksum. Needs to be requested using the filter parameter.', 'post_meta_checksum' => '(string|null) Post Meta checksum. Needs to be requested using the filter parameter.', 'comment_meta_checksum' => '(string|null) Comment Meta checksum. Needs to be requested using the filter parameter.', 'started' => '(int|null) The unix timestamp when the last sync started', 'queue_finished' => '(int|null) The unix timestamp when the enqueuing was done for the last sync', 'send_started' => '(int|null) The unix timestamp when the last send process started', 'finished' => '(int|null) The unix timestamp when the last sync finished', 'total' => '(array) Count of actions that could be sent', 'queue' => '(array) Count of actions that have been added to the queue', 'sent' => '(array) Count of actions that have been sent', 'config' => '(array) Configuration of the last full sync', 'queue_size' => '(int) Number of items in the sync queue', 'queue_lag' => '(float) Time delay of the oldest item in the sync queue', 'queue_next_sync' => '(float) Time in seconds before trying to sync again', 'full_queue_size' => '(int) Number of items in the full sync queue', 'full_queue_lag' => '(float) Time delay of the oldest item in the full sync queue', 'full_queue_next_sync' => '(float) Time in seconds before trying to sync the full sync queue again', 'cron_size' => '(int) Size of the current cron array', 'next_cron' => '(int) The number of seconds till the next item in cron.', 'progress' => '(array) Full Sync status by module', 'debug_details' => '(array) Details as to why Sync is disabled.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/status', ) ); // GET /sites/%s/data-checksums new Jetpack_JSON_API_Sync_Check_Endpoint( array( 'description' => 'Check that cacheable data on the site is in sync with wordpress.com', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/data-checksums', 'stat' => 'data-checksums', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'posts' => '(string) Posts checksum', 'comments' => '(string) Comments checksum', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/data-checksums', ) ); // GET /sites/%s/data-histogram new Jetpack_JSON_API_Sync_Histogram_Endpoint( array( 'description' => 'Get a histogram of checksums for certain synced data', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/data-histogram', 'stat' => 'data-histogram', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'object_type' => '(string=posts) The type of object to checksum - posts, comments or options', 'buckets' => '(int=10) The number of buckets for the checksums', 'start_id' => '(int=0) Starting ID for the range', 'end_id' => '(int=null) Ending ID for the range', 'columns' => '(string) Columns to checksum', 'strip_non_ascii' => '(bool=true) Strip non-ascii characters from all columns', 'shared_salt' => '(string) Salt to reduce the collision and improve validation', 'only_range_edges' => '(bool=false) Only return the edges of the specified range', 'detailed_drilldown' => '(bool=false) Return a detailed drilldown in `key => checksum` format', ), 'response_format' => array( 'histogram' => '(array) Associative array of histograms by ID range, e.g. "500-999" => "abcd1234"', 'type' => '(string) Type of checksum algorithm', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/data-histogram', ) ); // POST /sites/%s/sync/health . new Jetpack_JSON_API_Sync_Modify_Health_Endpoint( array( 'description' => 'Update sync health', 'method' => 'POST', 'group' => '__do_not_document', 'path' => '/sites/%s/sync/health', 'stat' => 'write-sync-health', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'status' => '(string) Sync Health Status of site', ), 'response_format' => array( 'response' => '(string) Current Sync Health ', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/health', ) ); $sync_settings_response = array( 'dequeue_max_bytes' => '(int|bool=false) Maximum bytes to read from queue in a single request', 'sync_wait_time' => '(int|bool=false) Wait time between requests in seconds if sync threshold exceeded', 'sync_wait_threshold' => '(int|bool=false) If a request to WPCOM exceeds this duration, wait sync_wait_time seconds before sending again', 'upload_max_bytes' => '(int|bool=false) Maximum bytes to send in a single request', 'upload_max_rows' => '(int|bool=false) Maximum rows to send in a single request', 'max_queue_size' => '(int|bool=false) Maximum queue size that that the queue is allowed to expand to in DB rows to prevent the DB from filling up. Needs to also meet the max_queue_lag limit.', 'max_queue_lag' => '(int|bool=false) Maximum queue lag in seconds used to prevent the DB from filling up. Needs to also meet the max_queue_size limit.', 'queue_max_writes_sec' => '(int|bool=false) Maximum writes per second to allow to the queue during full sync.', 'post_types_blacklist' => '(array|string|bool=false) List of post types to exclude from sync. Send "empty" to unset.', 'post_meta_whitelist' => '(array|string|bool=false) List of post meta to be included in sync. Send "empty" to unset.', 'comment_meta_whitelist' => '(array|string|bool=false) List of comment meta to be included in sync. Send "empty" to unset.', 'disable' => '(int|bool=false) Set to 1 or true to disable sync entirely.', 'checksum_disable' => '(int|bool=false) Set to 1 or true to disable checksums entirely.', 'render_filtered_content' => '(int|bool=true) Set to 1 or true to render filtered content.', 'max_enqueue_full_sync' => '(int|bool=false) Maximum number of rows to enqueue during each full sync process', 'max_queue_size_full_sync' => '(int|bool=false) Maximum queue size that full sync is allowed to use', 'full_sync_send_duration' => '(float) Max full sync duration per request', 'sync_via_cron' => '(int|bool=false) Set to 1 or true to avoid using cron for sync.', 'cron_sync_time_limit' => '(int|bool=false) Limit cron jobs to number of seconds', 'enqueue_wait_time' => '(int|bool=false) Wait time in seconds between attempting to continue a full sync, via requests', 'sync_sender_enabled' => '(int|bool=false) Set to 1 or true to enable the default sender for the incremental queue.', 'full_sync_sender_enabled' => '(int|bool=false) Set to 1 or true to enable the default sender for the "full sync" queue.', ); // GET /sites/%s/sync/settings new Jetpack_JSON_API_Sync_Get_Settings_Endpoint( array( 'description' => 'Update sync settings', 'method' => 'GET', 'group' => '__do_not_document', 'path' => '/sites/%s/sync/settings', 'stat' => 'write-sync-settings', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => $sync_settings_response, 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/settings', ) ); // POST /sites/%s/sync/settings new Jetpack_JSON_API_Sync_Modify_Settings_Endpoint( array( 'description' => 'Update sync settings', 'method' => 'POST', 'group' => '__do_not_document', 'path' => '/sites/%s/sync/settings', 'stat' => 'write-sync-settings', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => $sync_settings_response, 'response_format' => $sync_settings_response, 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/settings', ) ); // GET /sites/%s/sync/object new Jetpack_JSON_API_Sync_Object( array( 'description' => 'Get an object by ID from one of the sync modules, in the format it would be synced in', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/sync/object', 'stat' => 'sync-object', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'module_name' => '(string) The sync module ID, e.g. "posts"', 'object_type' => '(string) An identified for the object type, e.g. "post"', 'object_ids' => '(array) The IDs of the objects', ), 'response_format' => array( 'objects' => '(string) The encoded objects', 'codec' => '(string) The codec used to encode the objects, deflate-json-array or simple', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/object?module_name=posts&object_type=post&object_ids[]=1&object_ids[]=2&object_ids[]=3', ) ); // POST /sites/%s/sync/now new Jetpack_JSON_API_Sync_Now_Endpoint( array( 'description' => 'Force immediate sync of top items on a queue', 'method' => 'POST', 'path' => '/sites/%s/sync/now', 'stat' => 'sync-now', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'queue' => '(string) sync or full_sync', ), 'response_format' => array( 'response' => '(array) The response from the server', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/now?queue=full_sync', ) ); // POST /sites/%s/sync/unlock new Jetpack_JSON_API_Sync_Unlock_Endpoint( array( 'description' => 'Unlock the queue in case it gets locked by a process.', 'method' => 'POST', 'path' => '/sites/%s/sync/unlock', 'group' => '__do_not_document', 'stat' => 'sync-unlock', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'queue' => '(string) sync or full_sync', ), 'response_format' => array( 'success' => '(bool) Unlocking the queue successful?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/unlock', ) ); // GET /sites/%s/sync/object-id-range new Jetpack_JSON_API_Sync_Object_Id_Range( array( 'description' => 'Gets minimum and maximum object ids for each batch of given batch size.', 'method' => 'GET', 'path' => '/sites/%s/sync/object-id-range', 'group' => '__do_not_document', 'stat' => 'sync-object-id-range', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'batch_size' => '(int=1000) The amount of objects per batch.', 'sync_module' => '(string=posts) The sync module used to enumerate the ranges.', ), 'response_format' => array( 'ranges' => '(array) An array of range objects with min and max properties for each batch.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/object-id-range?batch_size=100&sync_module=comments', ) ); // POST /sites/%s/sync/checkout new Jetpack_JSON_API_Sync_Checkout_Endpoint( array( 'description' => 'Locks the queue and returns items and the buffer ID.', 'method' => 'POST', 'path' => '/sites/%s/sync/checkout', 'group' => '__do_not_document', 'stat' => 'sync-checkout', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'queue' => '(string) sync or full_sync', 'number_of_items' => '(int=10) Maximum number of items from the queue to be returned', 'encode' => '(bool=true) Use the default encode method', 'force' => '(bool=false) Force unlock the queue', 'pop' => '(bool=false) Pop from the queue without checkout, use carefully 😱', ), 'response_format' => array( 'buffer_id' => '(string) Buffer ID that we are using', 'items' => '(array) Items from the queue that are ready to be processed by the sync server', 'skipped_items' => '(array) Skipped item ids', 'codec' => '(string) The name of the codec used to encode the data', 'sent_timestamp' => '(int) Current timestamp of the server', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/checkout', ) ); // POST /sites/%s/sync/close new Jetpack_JSON_API_Sync_Close_Endpoint( array( 'description' => 'Closes the buffer and delete the processed items from the queue.', 'method' => 'POST', 'path' => '/sites/%s/sync/close', 'group' => '__do_not_document', 'stat' => 'sync-close', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'item_ids' => '(array) Item IDs to delete from the queue.', 'queue' => '(string) sync or full_sync', 'buffer_id' => '(string) buffer ID that was opened during the checkout step.', 'continue' => '(bool=false) Perform another checkout from queue.', ), 'response_format' => array( 'success' => '(bool) Closed the buffer successfully?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/sync/close', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-log-endpoint.php'; new Jetpack_JSON_API_Jetpack_Log_Endpoint( array( 'description' => 'Get the Jetpack log', 'method' => 'GET', 'path' => '/sites/%s/jetpack-log', 'stat' => 'log', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'event' => '(string) The event to filter by, by default all entries are returned', 'num' => '(int) The number of entries to get, by default all entries are returned', ), 'response_format' => array( 'log' => '(array) An array of jetpack log entries', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/jetpack-log', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-maybe-auto-update-endpoint.php'; new Jetpack_JSON_API_Maybe_Auto_Update_Endpoint( array( 'description' => 'Maybe Auto Update Core, Plugins, Themes and Languages', 'method' => 'POST', 'path' => '/sites/%s/maybe-auto-update', 'stat' => 'maybe-auto-update', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'log' => '(array) Results of running the update job', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/maybe-auto-update', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-translations-endpoint.php'; require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-translations-modify-endpoint.php'; new Jetpack_JSON_API_Translations_Endpoint( array( 'description' => 'Gets info about a Jetpack blog\'s core installation', 'method' => 'GET', 'path' => '/sites/%s/translations', 'stat' => 'translations', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'translations' => '(array) A list of translations that are available', 'autoupdate' => '(bool) Whether or not we automatically update translations', 'log' => '(array:safehtml) An array of log strings.', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/translations', ) ); new Jetpack_JSON_API_Translations_Modify_Endpoint( array( 'description' => 'Toggle automatic core updates for a Jetpack blog', 'method' => 'POST', 'path' => '/sites/%s/translations', 'stat' => 'translations', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'autoupdate' => '(bool) Whether or not we automatically update translations', ), 'response_format' => array( 'translations' => '(array) A list of translations that are available', 'autoupdate' => '(bool) Whether or not we automatically update translations', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'autoupdate' => true, ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/translations', ) ); new Jetpack_JSON_API_Translations_Modify_Endpoint( array( 'description' => 'Update All Translations installation on a Jetpack blog', 'method' => 'POST', 'path' => '/sites/%s/translations/update', 'stat' => 'translations:update', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'log' => '(array:safehtml) An array of log strings.', 'success' => '(bool) Was the operation successful', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/translations/update', ) ); // Options require_once $json_jetpack_endpoints_dir . 'class.wpcom-json-api-get-option-endpoint.php'; new WPCOM_JSON_API_Get_Option_Endpoint( array( 'method' => 'GET', 'description' => 'Fetches an option.', 'group' => '__do_not_document', 'stat' => 'option', 'allow_jetpack_site_auth' => true, 'path' => '/sites/%s/option', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'option_name' => '(string) The name of the option to fetch.', 'site_option' => '(bool=false) True if the option is a site option.', ), 'response_format' => array( 'option_value' => '(string|object) The value of the option.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/option?option_name=blogname', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); require_once $json_jetpack_endpoints_dir . 'class.wpcom-json-api-update-option-endpoint.php'; new WPCOM_JSON_API_Update_Option_Endpoint( array( 'method' => 'POST', 'description' => 'Updates an option.', 'group' => '__do_not_document', 'stat' => 'option:update', 'allow_jetpack_site_auth' => true, 'path' => '/sites/%s/option', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'option_name' => '(string) The name of the option to fetch.', 'site_option' => '(bool=false) True if the option is a site option.', 'is_array' => '(bool=false) True if the value should be converted to an array before saving.', ), 'request_format' => array( 'option_value' => '(string|object) The new value of the option.', ), 'response_format' => array( 'option_value' => '(string|object) The value of the updated option.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/option', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'option_value' => 'My new blog name', ), ), ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-cron-endpoint.php'; // GET /sites/%s/cron new Jetpack_JSON_API_Cron_Endpoint( array( 'description' => 'Fetches the cron array', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/cron', 'stat' => 'cron-get', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'cron_array' => '(array) The cron array', 'current_timestamp' => '(int) Current server timestamp', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), ) ); // POST /sites/%s/cron new Jetpack_JSON_API_Cron_Post_Endpoint( array( 'description' => 'Process items in the cron', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/cron', 'stat' => 'cron-run', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'hooks' => '(array) List of hooks to run if they have been scheduled (optional)', ), 'response_format' => array( 'success' => '(array) Of processed hooks with their arguments', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'hooks' => array( 'jetpack_sync_cron' ), ), ), ) ); // POST /sites/%s/cron/schedule new Jetpack_JSON_API_Cron_Schedule_Endpoint( array( 'description' => 'Schedule one or a recurring hook to fire at a particular time', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/cron/schedule', 'stat' => 'cron-schedule', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'hook' => '(string) Hook name that should run when the event is scheduled', 'timestamp' => '(int) Timestamp when the event should take place, has to be in the future', 'arguments' => '(string) JSON Object of arguments that the hook will use (optional)', 'recurrence' => '(string) How often the event should take place. If empty only one event will be scheduled. Possible values 1min, hourly, twicedaily, daily (optional) ', ), 'response_format' => array( 'success' => '(bool) Was the event scheduled?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron/schedule', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'hook' => 'jetpack_sync_cron', 'arguments' => '[]', 'recurrence' => '1min', 'timestamp' => 1476385523, ), ), ) ); // POST /sites/%s/cron/unschedule new Jetpack_JSON_API_Cron_Unschedule_Endpoint( array( 'description' => 'Unschedule one or all events with a particular hook and arguments', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/cron/unschedule', 'stat' => 'cron-unschedule', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'hook' => '(string) Name of the hook that should be unscheduled', 'timestamp' => '(int) Timestamp of the hook that you want to unschedule. This will unschedule only 1 event. (optional)', 'arguments' => '(string) JSON Object of arguments that the hook has been scheduled with (optional)', ), 'response_format' => array( 'success' => '(bool) Was the event unscheduled?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/example.wordpress.org/cron/unschedule', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'hook' => 'jetpack_sync_cron', 'arguments' => '[]', 'timestamp' => 1476385523, ), ), ) ); // BACKUPS // GET /sites/%s/database-object/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-database-object-backup-endpoint.php'; new Jetpack_JSON_API_Get_Database_Object_Backup_Endpoint( array( 'description' => 'Fetch a backup of a database object, along with all of its metadata', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/database-object/backup', 'stat' => 'database-objects:1:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'object_type' => '(string) Type of object to fetch from the database', 'object_id' => '(int) ID of the database object to fetch', ), 'response_format' => array( 'object' => '(array) Database object row', 'meta' => '(array) Associative array of key/value metadata associated with the row', 'children' => '(array) Where appropriate, child records associated with the object. eg: Woocommerce tax rate locations', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/database-object/backup', ) ); // GET /sites/%s/comments/%d/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-comment-backup-endpoint.php'; new Jetpack_JSON_API_Get_Comment_Backup_Endpoint( array( 'description' => 'Fetch a backup of a comment, along with all of its metadata', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/comments/%d/backup', 'stat' => 'comments:1:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$post' => '(int) The comment ID', ), 'response_format' => array( 'comment' => '(array) Comment table row', 'meta' => '(array) Associative array of key/value commentmeta data', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/comments/1/backup', ) ); // GET /sites/%s/options/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-option-backup-endpoint.php'; new Jetpack_JSON_API_Get_Option_Backup_Endpoint( array( 'description' => 'Fetch a backup of an option', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/options/backup', 'stat' => 'options:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'name' => '(string|array) One or more option names to include in the backup', ), 'response_format' => array( 'options' => '(array) Associative array of option_name => option_value entries', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/options/backup', ) ); // GET /sites/%s/posts/%d/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-post-backup-endpoint.php'; new Jetpack_JSON_API_Get_Post_Backup_Endpoint( array( 'description' => 'Fetch a backup of a post, along with all of its metadata', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/posts/%d/backup', 'stat' => 'posts:1:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$post' => '(int) The post ID', ), 'response_format' => array( 'post' => '(array) Post table row', 'meta' => '(array) Associative array of key/value postmeta data', 'terms' => '(array) List of terms attached to the post object', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/posts/1/backup', ) ); // GET /sites/%s/terms/%d/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-term-backup-endpoint.php'; new Jetpack_JSON_API_Get_Term_Backup_Endpoint( array( 'description' => 'Fetch a backup of a term, along with all of its metadata', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/terms/%d/backup', 'stat' => 'terms:1:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$term' => '(int) The term ID', ), 'response_format' => array( 'term' => '(array) Term table row', 'meta' => '(array) Metadata associated with the term', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/terms/1/backup', ) ); // GET /sites/%s/users/%d/backup require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-get-user-backup-endpoint.php'; new Jetpack_JSON_API_Get_User_Backup_Endpoint( array( 'description' => 'Fetch a backup of a user, along with all of its metadata', 'group' => '__do_not_document', 'method' => 'GET', 'path' => '/sites/%s/users/%d/backup', 'stat' => 'users:1:backup', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$user' => '(int) The user ID', ), 'response_format' => array( 'user' => '(array) User table row', 'meta' => '(array) Associative array of key/value usermeta data', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/users/1/backup', ) ); // USERS require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-user-connect-endpoint.php'; // POST /sites/%s/users/%d/connect new Jetpack_JSON_API_User_Connect_Endpoint( array( 'description' => 'Creates or returns a new user given profile data', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/users/%d/connect', 'stat' => 'users:connect', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$user_id' => '(int) The site user ID to connect', ), 'request_format' => array( 'user_token' => '(string) The user token', ), 'response_format' => array( 'success' => '(bool) Was the user connected', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'user_token' => 'XDH55jndskjf3klh3', ), ), 'example_response' => '{ "success" => true }', 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/users/6/connect', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-user-create-endpoint.php'; // POST /sites/%s/users/create new Jetpack_JSON_API_User_Create_Endpoint( array( 'description' => 'Creates or returns a new user given profile data', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/users/create', 'stat' => 'users:create', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'query_parameters' => array( 'invite_accepted' => '(bool=false) If the user is being created in the invite context', ), 'request_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'roles' => array( array( 'administrator', ), ), 'first_name' => 'John', 'last_name' => 'Doe', 'email' => 'john.doe@example.wordpress.org', ), ), 'example_response' => '{ "ID": 18342963, "login": "binarysmash" "email": false, "name": "binarysmash", "URL": "http:\/\/binarysmash.wordpress.com", "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G", "profile_URL": "http:\/\/gravatar.com\/binarysmash", "roles": [ "administrator" ] }', 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/users/create', ) ); require_once $json_jetpack_endpoints_dir . 'class.jetpack-json-api-jps-woocommerce-connect-endpoint.php'; // POST /sites/%s/jps/woo-connect new Jetpack_JSON_API_JPS_WooCommerce_Connect_Endpoint( array( 'description' => 'Attempts to connect the WooCommerce plugin for this site to WooCommerce.com.', 'group' => '__do_not_document', 'method' => 'POST', 'path' => '/sites/%s/jps/woo-connect', 'stat' => 'jps:woo-connect', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'access_token' => '(string) The access token for WooCommerce to connect to WooCommerce.com', 'access_token_secret' => '(string) The access token secret for WooCommerce to connect to WooCommerce.com', 'user_id' => '(int) The user\'s ID after registering for a host plan', 'site_id' => '(int) The site\'s ID after registering for a host plan', ), 'response_format' => array( 'success' => '(bool) Setting access token and access token secret successful?', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'access_token' => '123456789', 'access_token_secret' => 'abcdefghiklmnop', 'user_id' => 1, 'site_id' => 2, ), ), 'example_response' => '{ "success": true }', 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/jps/woo-connect', ) ); // POST /sites/%s/install-backup-helper-script require_once $json_jetpack_endpoints_dir . 'class-jetpack-json-api-install-backup-helper-script-endpoint.php'; new Jetpack_JSON_API_Install_Backup_Helper_Script_Endpoint( array( 'description' => 'Setup a Helper Script, to allow Jetpack Backup to connect to this site', 'group' => '__do_not_document', 'method' => 'POST', 'stat' => 'install-backup-helper-script', 'path' => '/sites/%s/install-backup-helper-script', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'helper' => '(string) Base64-encoded Helper Script contents', ), 'response_format' => array( 'abspath' => '(string) WordPress install path', 'path' => '(string) Path of the helper script', 'url' => '(string) URL to access the helper script', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/install-backup-helper-script', ) ); // POST /sites/%s/delete-backup-helper-script require_once $json_jetpack_endpoints_dir . 'class-jetpack-json-api-delete-backup-helper-script-endpoint.php'; new Jetpack_JSON_API_Delete_Backup_Helper_Script_Endpoint( array( 'description' => 'Delete a Helper Script', 'group' => '__do_not_document', 'method' => 'POST', 'stat' => 'delete-backup-helper-script', 'path' => '/sites/%s/delete-backup-helper-script', 'allow_jetpack_site_auth' => true, 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'response_format' => array( 'success' => '(bool) Deleted the Helper Script successfully?', ), 'request_format' => array( 'path' => '(string) Path to Helper Script to delete', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/delete-backup-helper-script', ) ); jetpack/class.jetpack-json-api-core-endpoint.php 0000644 00000001501 14722054026 0015704 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Core endpoint class. * * POST /sites/%s/core * POST /sites/%s/core/update */ class Jetpack_JSON_API_Core_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'manage_options'; /** * New version. * * @var string */ protected $new_version; /** * An array of log strings. * * @var array */ protected $log; /** * Return the result of the wp_version. * * @return array */ public function result() { global $wp_version; return array( 'version' => ( empty( $this->new_version ) ) ? $wp_version : $this->new_version, 'autoupdate' => Jetpack_Options::get_option( 'autoupdate_core', false ), 'log' => $this->log, ); } } jetpack/class.jetpack-json-api-themes-install-endpoint.php 0000644 00000014551 14722054026 0017716 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; use Automattic\Jetpack\Automatic_Install_Skin; use Automattic\Jetpack\Connection\Client; /** * Themes install endpoint class. * * POST /sites/%s/themes/%s/install */ class Jetpack_JSON_API_Themes_Install_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'install_themes'; /** * The action. * * @var string */ protected $action = 'install'; /** * Download links. * * @var array */ protected $download_links = array(); /** * Install the theme. * * @return bool|WP_Error */ protected function install() { foreach ( $this->themes as $theme ) { /** * Filters whether to use an alternative process for installing a WordPress.com theme. * The alternative process can be executed during the filter. * * The filter can also return an instance of WP_Error; in which case the endpoint response will * contain this error. * * @module json-api * * @since 4.4.2 * * @param bool $use_alternative_install_method Whether to use the alternative method of installing * a WPCom theme. * @param string $theme_slug Theme name (slug). If it is a WPCom theme, * it should be suffixed with `-wpcom`. */ $result = apply_filters( 'jetpack_wpcom_theme_install', false, $theme ); $skin = null; $upgrader = null; $link = null; // If the alternative install method was not used, use the standard method. if ( ! $result ) { $skin = new Automatic_Install_Skin(); $upgrader = new Theme_Upgrader( $skin ); $link = $this->download_links[ $theme ]; $result = $upgrader->install( $link ); } if ( file_exists( $link ) ) { // Delete if link was tmp local file wp_delete_file( $link ); } if ( ! $this->bulk && is_wp_error( $result ) ) { return $result; } if ( ! $result ) { $error = __( 'An unknown error occurred during installation', 'jetpack' ); $this->log[ $theme ]['error'] = $error; } elseif ( ! self::is_installed_theme( $theme ) ) { $error = __( 'There was an error installing your theme', 'jetpack' ); $this->log[ $theme ]['error'] = $error; } elseif ( $upgrader ) { $this->log[ $theme ][] = $upgrader->skin->get_upgrade_messages(); } } if ( ! $this->bulk && isset( $error ) ) { return new WP_Error( 'install_error', $error, 400 ); } return true; } /** * Validate the themes. * * @return bool|WP_Error */ protected function validate_themes() { if ( empty( $this->themes ) || ! is_array( $this->themes ) ) { return new WP_Error( 'missing_themes', __( 'No themes found.', 'jetpack' ) ); } foreach ( $this->themes as $theme ) { if ( self::is_installed_theme( $theme ) ) { return new WP_Error( 'theme_already_installed', __( 'The theme is already installed', 'jetpack' ) ); } /** * Filters whether to skip the standard method of downloading and validating a WordPress.com * theme. An alternative method of WPCom theme download and validation can be * executed during the filter. * * The filter can also return an instance of WP_Error; in which case the endpoint response will * contain this error. * * @module json-api * * @since 4.4.2 * * @param bool $skip_download_filter_result Whether to skip the standard method of downloading * and validating a WPCom theme. * @param string $theme_slug Theme name (slug). If it is a WPCom theme, * it should be suffixed with `-wpcom`. */ $skip_download_filter_result = apply_filters( 'jetpack_wpcom_theme_skip_download', false, $theme ); if ( is_wp_error( $skip_download_filter_result ) ) { return $skip_download_filter_result; } elseif ( $skip_download_filter_result ) { continue; } if ( wp_endswith( $theme, '-wpcom' ) ) { $file = self::download_wpcom_theme_to_file( $theme ); if ( is_wp_error( $file ) ) { return $file; } $this->download_links[ $theme ] = $file; continue; } $params = (object) array( 'slug' => $theme ); $url = 'https://api.wordpress.org/themes/info/1.0/'; // @todo Switch to https://api.wordpress.org/themes/info/1.1/, which uses JSON rather than PHP serialization. $args = array( 'body' => array( 'action' => 'theme_information', 'request' => serialize( $params ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize ), ); $response = wp_remote_post( $url, $args ); $theme_data = unserialize( $response['body'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize if ( is_wp_error( $theme_data ) ) { return $theme_data; } if ( ! is_object( $theme_data ) && ! isset( $theme_data->download_link ) ) { return new WP_Error( 'theme_not_found', __( 'This theme does not exist', 'jetpack' ), 404 ); } $this->download_links[ $theme ] = $theme_data->download_link; } return true; } /** * Check if the theme is installed. * * @param string $theme - the theme we're checking. * * @return bool */ protected static function is_installed_theme( $theme ) { $wp_theme = wp_get_theme( $theme ); return $wp_theme->exists(); } /** * Download the wpcom theme. * * @param string $theme - the theme to download. * * @return string|WP_Error */ protected static function download_wpcom_theme_to_file( $theme ) { $file = wp_tempnam( 'theme' ); if ( ! $file ) { return new WP_Error( 'problem_creating_theme_file', __( 'Problem creating file for theme download', 'jetpack' ) ); } $url = "themes/download/$theme.zip"; $args = array( 'stream' => true, 'filename' => $file, ); $result = Client::wpcom_json_api_request_as_blog( $url, '1.1', $args ); $response = $result['response']; if ( $response['code'] !== 200 ) { wp_delete_file( $file ); return new WP_Error( 'problem_fetching_theme', __( 'Problem downloading theme', 'jetpack' ) ); } return $file; } } jetpack/class.jetpack-json-api-get-term-backup-endpoint.php 0000644 00000002467 14722054026 0017757 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Get Term backup endpoint class. * * /sites/%s/terms/%d/backup -> $blog_id, $term_id */ class Jetpack_JSON_API_Get_Term_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * The term ID. * * @var int */ protected $term_id; /** * Validate input. * * @param int $term_id - the term ID. * * @return bool|WP_Error */ public function validate_input( $term_id ) { if ( empty( $term_id ) || ! is_numeric( $term_id ) ) { return new WP_Error( 'term_id_not_specified', __( 'You must specify a Term ID', 'jetpack' ), 400 ); } $this->term_id = (int) $term_id; return true; } /** * Return the result. * * @return array|WP_Error */ protected function result() { // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $term = get_term( $this->term_id ); if ( empty( $term ) ) { return new WP_Error( 'term_not_found', __( 'Term not found', 'jetpack' ), 404 ); } return array( 'term' => (array) $term, 'meta' => get_term_meta( $this->term_id ), ); } } jetpack/class.jetpack-json-api-plugins-install-endpoint.php 0000644 00000007327 14722054026 0020115 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Plugins_Installer; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; // POST /sites/%s/plugins/%s/install new Jetpack_JSON_API_Plugins_Install_Endpoint( array( 'description' => 'Install a plugin to your jetpack blog', 'group' => '__do_not_document', 'stat' => 'plugins:1:install', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/install', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(int|string) The plugin slug to install', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/akismet/install', ) ); new Jetpack_JSON_API_Plugins_Install_Endpoint( array( 'description' => 'Install a plugin to your jetpack blog', 'group' => '__do_not_document', 'stat' => 'plugins:1:install', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/install', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(int|string) The plugin slug to install', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/akismet/install', ) ); /** * Plugins install enedpoint class. * * POST /sites/%s/plugins/%s/install */ class Jetpack_JSON_API_Plugins_Install_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'install_plugins'; /** * The action. * * @var string */ protected $action = 'install'; /** * Installation. * * @return bool|WP_Error */ protected function install() { $result = ''; foreach ( $this->plugins as $index => $slug ) { $result = Plugins_Installer::install_plugin( $slug ); if ( is_wp_error( $result ) ) { $this->log[ $slug ][] = $result->get_error_message(); if ( ! $this->bulk ) { return $result; } } } if ( ! $result ) { return new WP_Error( 'plugin_install_failed', __( 'Plugin install failed because the result was invalid.', 'jetpack' ) ); } if ( is_wp_error( $result ) ) { return $result; } // No errors, install worked. Now replace the slug with the actual plugin id $this->plugins[ $index ] = Plugins_Installer::get_plugin_id_by_slug( $slug ); return true; } /** * Validate the plugins. * * @return bool|WP_Error */ protected function validate_plugins() { if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) { return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) ); } foreach ( $this->plugins as $slug ) { // make sure it is not already installed if ( Plugins_Installer::get_plugin_id_by_slug( $slug ) ) { return new WP_Error( 'plugin_already_installed', __( 'The plugin is already installed', 'jetpack' ) ); } } return true; } } jetpack/class.jetpack-json-api-maybe-auto-update-endpoint.php 0000644 00000002211 14722054026 0020276 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Auto update endpoint class. * * POST /sites/%s/maybe_auto_update */ class Jetpack_JSON_API_Maybe_Auto_Update_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' ); /** * Update results. * * @var array */ protected $update_results = array(); /** * The result. * * @return array */ protected function result() { add_action( 'automatic_updates_complete', array( $this, 'get_update_results' ), 100, 1 ); wp_maybe_auto_update(); $result = array(); $result['log'] = $this->update_results; if ( empty( $result['log'] ) ) { $possible_reasons_for_failure = Jetpack_Autoupdate::get_possible_failures(); if ( $possible_reasons_for_failure ) { $result['log']['error'] = $possible_reasons_for_failure; } } return $result; } /** * Get update results. * * @param array $results - the results. */ public function get_update_results( $results ) { $this->update_results = $results; } } jetpack/class-jetpack-json-api-modules-list-v1-2-endpoint.php 0000644 00000002530 14722054026 0020062 0 ustar 00 <?php /** * List modules v1.2 endpoint. * * @package automattic/jetpack */ use Automattic\Jetpack\Status; /** * List modules v1.2 endpoint. */ class Jetpack_JSON_API_Modules_List_V1_2_Endpoint extends Jetpack_JSON_API_Endpoint { /** * This endpoint allows authentication both via a blog and a user token. * If a user token is used, that user should have `jetpack_manage_modules` capability. * * @var array|string */ protected $needed_capabilities = 'jetpack_manage_modules'; /** * Fetch modules list. * * @return array An array of module objects. */ protected function result() { require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; $is_offline_mode = ( new Status() )->is_offline_mode(); $modules = Jetpack_Admin::init()->get_modules(); foreach ( $modules as $slug => $properties ) { if ( $is_offline_mode ) { $requires_connection = isset( $modules[ $slug ]['requires_connection'] ) && $modules[ $slug ]['requires_connection']; $requires_user_connection = isset( $modules[ $slug ]['requires_user_connection'] ) && $modules[ $slug ]['requires_user_connection']; if ( $requires_connection || $requires_user_connection ) { $modules[ $slug ]['activated'] = false; } } } $modules = Jetpack::get_translated_modules( $modules ); return array( 'modules' => $modules ); } } jetpack/class.jetpack-json-api-jps-woocommerce-connect-endpoint.php 0000644 00000003641 14722054026 0021523 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * JPS WooCommerce connect endpoint. */ class Jetpack_JSON_API_JPS_WooCommerce_Connect_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'manage_options'; /** * The result. * * @return array|WP_Error */ public function result() { $input = $this->input(); $helper_data = get_option( 'woocommerce_helper_data', array() ); if ( ! empty( $helper_data['auth'] ) ) { return new WP_Error( 'already_configured', __( 'WooCommerce auth data is already set.', 'jetpack' ) ); } // Only update the auth field for `woocommerce_helper_data` instead of blowing out the entire option. $helper_data['auth'] = array( 'user_id' => $input['user_id'], 'site_id' => $input['site_id'], 'updated' => time(), 'access_token' => $input['access_token'], 'access_token_secret' => $input['access_token_secret'], ); $updated = update_option( 'woocommerce_helper_data', $helper_data ); return array( 'success' => $updated, ); } /** * Validate input. * * @param object $object - the object we're validating. * * @return bool|WP_Error */ public function validate_input( $object ) { $input = $this->input(); if ( empty( $input['access_token'] ) ) { return new WP_Error( 'input_error', __( 'access_token is required', 'jetpack' ) ); } if ( empty( $input['access_token_secret'] ) ) { return new WP_Error( 'input_error', __( 'access_token_secret is required', 'jetpack' ) ); } if ( empty( $input['user_id'] ) ) { return new WP_Error( 'input_error', __( 'user_id is required', 'jetpack' ) ); } if ( empty( $input['site_id'] ) ) { return new WP_Error( 'input_error', __( 'site_id is required', 'jetpack' ) ); } return parent::validate_input( $object ); } } jetpack/class.jetpack-json-api-cron-endpoint.php 0000644 00000022612 14722054026 0015723 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Cron endpoint class. * * GET /sites/%s/cron */ class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'manage_options'; /** * Validate the call. * * @param int $_blog_id - the blog ID. * @param array $capability - the capabilities of the user. * @param bool $check_manage_active - parameter is for making the method signature compatible with its parent class method. */ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable return parent::validate_call( $_blog_id, $capability, false ); } /** * Return the result of current timestamp. */ protected function result() { return array( 'cron_array' => _get_cron_array(), 'current_timestamp' => time(), ); } /** * Sanitize the hook. * * @param string $hook - the hook. * * @return string */ protected function sanitize_hook( $hook ) { return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook ); } /** * Resolve arguments. * * @return array */ protected function resolve_arguments() { $args = $this->input(); return isset( $args['arguments'] ) ? json_decode( $args['arguments'] ) : array(); } /** * Check the cron lock. * * @param float $gmt_time - the time in GMT. * * @return string|int|WP_Error WP_Error if cron was locked in the `WP_CRON_LOCK_TIMEOUT` seconds before `gmt_time`, int or string otherwise. */ protected function is_cron_locked( $gmt_time ) { // The cron lock: a unix timestamp from when the cron was spawned. $doing_cron_transient = $this->get_cron_lock(); if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) ) { return new WP_Error( 'cron-is-locked', 'Current there is a cron already happening.', 403 ); } return $doing_cron_transient; } /** * Check if we can unlock the cron transient. * * @param string $doing_wp_cron - if we're doing the wp_cron. */ protected function maybe_unlock_cron( $doing_wp_cron ) { if ( $this->get_cron_lock() === $doing_wp_cron ) { delete_transient( 'doing_cron' ); } } /** * Set the cron lock. * * @return string */ protected function lock_cron() { $lock = sprintf( '%.22F', microtime( true ) ); set_transient( 'doing_cron', $lock ); return $lock; } /** * Get scheduled. * * @param string $hook - the hook. * @param array $args - the arguments. * * @return array */ protected function get_schedules( $hook, $args ) { $crons = _get_cron_array(); $key = md5( serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize if ( empty( $crons ) ) { return array(); } $found = array(); foreach ( $crons as $timestamp => $cron ) { if ( isset( $cron[ $hook ][ $key ] ) ) { $found[] = $timestamp; } } return $found; } /** * This function is based on the one found in wp-cron.php with a similar name * * @return int */ protected function get_cron_lock() { global $wpdb; $value = 0; if ( wp_using_ext_object_cache() ) { /* * Skip local cache and force re-fetch of doing_cron transient * in case another process updated the cache. */ $value = wp_cache_get( 'doing_cron', 'transient', true ); } else { $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) ); if ( is_object( $row ) ) { $value = $row->option_value; } } return $value; } } /** * Cron post endpoint class. * * POST /sites/%s/cron */ class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * The result. * * @return array|WP_Error */ protected function result() { define( 'DOING_CRON', true ); set_time_limit( 0 ); $args = $this->input(); $crons = _get_cron_array(); if ( false === $crons ) { return new WP_Error( 'no-cron-event', 'Currently there are no cron events', 400 ); } $timestamps_to_run = array_keys( $crons ); $gmt_time = microtime( true ); if ( isset( $timestamps_to_run[0] ) && $timestamps_to_run[0] > $gmt_time ) { return new WP_Error( 'no-cron-event', 'Currently there are no cron events ready to be run', 400 ); } $locked = $this->is_cron_locked( $gmt_time ); if ( is_wp_error( $locked ) ) { return $locked; } $lock = $this->lock_cron(); $processed_events = array(); foreach ( $crons as $timestamp => $cronhooks ) { if ( $timestamp > $gmt_time && ! isset( $args['hook'] ) ) { break; } foreach ( $cronhooks as $hook => $hook_data ) { if ( isset( $args['hook'] ) && ! in_array( $hook, $args['hook'], true ) ) { continue; } foreach ( $hook_data as $hook_item ) { $schedule = $hook_item['schedule']; $arguments = $hook_item['args']; if ( ! $schedule ) { wp_reschedule_event( $timestamp, $schedule, $hook, $arguments ); } wp_unschedule_event( $timestamp, $hook, $arguments ); do_action_ref_array( $hook, $arguments ); $processed_events[] = array( $hook => $arguments ); // If the hook ran too long and another cron process stole the lock, // or if we things are taking longer then 20 seconds then quit. if ( ( $this->get_cron_lock() !== $lock ) || ( $gmt_time + 20 > microtime( true ) ) ) { $this->maybe_unlock_cron( $lock ); return array( 'success' => $processed_events ); } } } } $this->maybe_unlock_cron( $lock ); return array( 'success' => $processed_events ); } } /** * Schedule endpoint class. * * POST /sites/%s/cron/schedule */ class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * The result. * * @return array|WP_Error */ protected function result() { $args = $this->input(); if ( ! isset( $args['timestamp'] ) ) { return new WP_Error( 'missing_argument', 'Please provide the timestamp argument', 400 ); } if ( ! is_int( $args['timestamp'] ) || $args['timestamp'] < time() ) { return new WP_Error( 'timestamp-invalid', 'Please provide timestamp that is an integer and set in the future', 400 ); } if ( ! isset( $args['hook'] ) ) { return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 ); } $hook = $this->sanitize_hook( $args['hook'] ); $locked = $this->is_cron_locked( microtime( true ) ); if ( is_wp_error( $locked ) ) { return $locked; } $arguments = $this->resolve_arguments(); $next_scheduled = $this->get_schedules( $hook, $arguments ); if ( isset( $args['recurrence'] ) ) { $schedules = wp_get_schedules(); if ( ! isset( $schedules[ $args['recurrence'] ] ) ) { return new WP_Error( 'invalid-recurrence', 'Please provide a valid recurrence argument', 400 ); } if ( is_countable( $next_scheduled ) && count( $next_scheduled ) > 0 ) { return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 ); } $lock = $this->lock_cron(); wp_schedule_event( $args['timestamp'], $args['recurrence'], $hook, $arguments ); $this->maybe_unlock_cron( $lock ); return array( 'success' => true ); } foreach ( $next_scheduled as $scheduled_time ) { if ( abs( $scheduled_time - $args['timestamp'] ) <= 10 * MINUTE_IN_SECONDS ) { return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 ); } } $lock = $this->lock_cron(); $next = wp_schedule_single_event( $args['timestamp'], $hook, $arguments ); $this->maybe_unlock_cron( $lock ); return array( 'success' => $next ); } } /** * The cron unschedule ednpoint class. * * POST /sites/%s/cron/unschedule */ class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * The result. * * @return array|WP_Error */ protected function result() { $args = $this->input(); if ( ! isset( $args['hook'] ) ) { return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 ); } $hook = $this->sanitize_hook( $args['hook'] ); $locked = $this->is_cron_locked( microtime( true ) ); if ( is_wp_error( $locked ) ) { return $locked; } $crons = _get_cron_array(); if ( empty( $crons ) ) { return new WP_Error( 'cron-not-present', 'Unable to unschedule an event, no events in the cron', 400 ); } $arguments = $this->resolve_arguments(); if ( isset( $args['timestamp'] ) ) { $next_schedulded = $this->get_schedules( $hook, $arguments ); if ( in_array( $args['timestamp'], $next_schedulded, true ) ) { return new WP_Error( 'event-not-present', 'Unable to unschedule the event, the event doesn\'t exist', 400 ); } $lock = $this->lock_cron(); wp_unschedule_event( $args['timestamp'], $hook, $arguments ); $this->maybe_unlock_cron( $lock ); return array( 'success' => true ); } $lock = $this->lock_cron(); wp_clear_scheduled_hook( $hook, $arguments ); $this->maybe_unlock_cron( $lock ); return array( 'success' => true ); } } jetpack/class.jetpack-json-api-plugins-list-endpoint.php 0000644 00000002734 14722054026 0017417 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new Jetpack_JSON_API_Plugins_List_Endpoint( array( 'description' => 'Get installed Plugins on your blog', 'method' => 'GET', 'path' => '/sites/%s/plugins', 'stat' => 'plugins', 'min_version' => '1', 'max_version' => '1.1', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'allow_jetpack_site_auth' => true, 'response_format' => array( 'plugins' => '(plugin) An array of plugin objects.', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins', ) ); /** * Plugins list endpoint class. * * GET /sites/%s/plugins * * No v1.2 versions since they are .com only */ class Jetpack_JSON_API_Plugins_List_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'activate_plugins'; /** * Validate the input. * * @param string $plugin - the plugin. * * @return bool */ public function validate_input( $plugin ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable wp_update_plugins(); $this->plugins = array_keys( get_plugins() ); return true; } } jetpack/class-jetpack-json-api-install-backup-helper-script-endpoint.php 0000644 00000004704 14722054026 0022453 0 ustar 00 <?php /** * API endpoint /sites/%s/install-backup-helper-script * This API endpoint installs a Helper Script to assist Jetpack Backup fetch data * * @package automattic/jetpack */ use Automattic\Jetpack\Backup\V0004\Helper_Script_Manager; /** * API endpoint /sites/%s/install-backup-helper-script * This API endpoint installs a Helper Script to assist Jetpack Backup fetch data */ class Jetpack_JSON_API_Install_Backup_Helper_Script_Endpoint extends Jetpack_JSON_API_Endpoint { /** * This endpoint is only accessible from Jetpack Backup; it requires no further capabilities. * * @var array */ protected $needed_capabilities = array(); /** * Method to call when running this endpoint (install) * * @var string */ protected $action = 'install'; /** * Contents of the Helper Script to install * * @var string|null */ protected $helper_script = null; /** * Contains the result of installing the Helper Script. * * @var null|WP_Error|array */ protected $result = null; /** * Checks that the input args look like a valid Helper Script. * * @param null $object Unused. * @return bool|WP_Error a WP_Error object or true if the input seems ok. */ protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $args = $this->input(); if ( ! isset( $args['helper'] ) ) { return new WP_Error( 'invalid_args', __( 'You must specify a helper script body', 'jetpack' ), 400 ); } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $this->helper_script = base64_decode( $args['helper'] ); if ( ! $this->helper_script ) { return new WP_Error( 'invalid_args', __( 'Helper script body must be base64 encoded', 'jetpack' ), 400 ); } return true; } /** * Installs the uploaded Helper Script. */ protected function install() { $this->result = Helper_Script_Manager::install_helper_script( $this->helper_script ); Helper_Script_Manager::cleanup_expired_helper_scripts(); } /** * Return the success or failure of the backup helper script installation operation. * * @return array|WP_Error An array with installation info on success: * * 'path' (string) Helper script installation path on the filesystem. * 'url' (string) URL to the helper script. * 'abspath' (string) WordPress root. * * or an instance of WP_Error on failure. */ protected function result() { return $this->result; } } jetpack/class.jetpack-json-api-themes-delete-endpoint.php 0000644 00000004255 14722054026 0017512 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Themes delete endpoint class. * POST /sites/%s/plugins/%s/delete */ class Jetpack_JSON_API_Themes_Delete_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'delete_themes'; /** * The action. * * @var string */ protected $action = 'delete'; /** * Delete the theme. * * @return bool|WP_Error */ protected function delete() { foreach ( $this->themes as $theme ) { // Don't delete an active child theme if ( is_child_theme() && $theme === get_stylesheet() ) { $error = 'You cannot delete a theme while it is active on the main site.'; $this->log[ $theme ]['error'] = $error; continue; } if ( $theme === get_template() ) { $error = 'You cannot delete a theme while it is active on the main site.'; $this->log[ $theme ]['error'] = $error; continue; } /** * Filters whether to use an alternative process for deleting a WordPress.com theme. * The alternative process can be executed during the filter. * * The filter can also return an instance of WP_Error; in which case the endpoint response will * contain this error. * * @module json-api * * @since 4.4.2 * * @param bool $use_alternative_delete_method Whether to use the alternative method of deleting * a WPCom theme. * @param string $theme_slug Theme name (slug). If it is a WPCom theme, * it should be suffixed with `-wpcom`. */ $result = apply_filters( 'jetpack_wpcom_theme_delete', false, $theme ); if ( ! $result ) { $result = delete_theme( $theme ); } if ( is_wp_error( $result ) ) { $error = $result->get_error_messages(); $this->log[ $theme ]['error'] = $error; } else { $this->log[ $theme ][] = 'Theme deleted'; } } if ( ! $this->bulk && isset( $error ) ) { return new WP_Error( 'delete_theme_error', $error, 400 ); } return true; } } jetpack/class.jetpack-json-api-themes-active-endpoint.php 0000644 00000004412 14722054026 0017516 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * GET /sites/%s/themes/mine => current theme * POST /sites/%s/themes/mine => switch theme */ class Jetpack_JSON_API_Themes_Active_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Endpoint callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param object $object - The unused $object parameter is for making the method signature compatible with its parent class method. * * @return array|bool|WP_Error */ public function callback( $path = '', $blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $error = $this->validate_call( $blog_id, 'switch_themes', true ); if ( is_wp_error( $error ) ) { return $error; } if ( 'POST' === $this->api->method ) { return $this->switch_theme(); } else { return $this->get_current_theme(); } } /** * Switch the theme. * * @return array|WP_Error */ protected function switch_theme() { $args = $this->input(); if ( ! isset( $args['theme'] ) || empty( $args['theme'] ) ) { return new WP_Error( 'missing_theme', __( 'You are required to specify a theme to switch to.', 'jetpack' ), 400 ); } $theme_slug = $args['theme']; if ( ! $theme_slug ) { return new WP_Error( 'theme_not_found', __( 'Theme is empty.', 'jetpack' ), 404 ); } /** * Trigger action before the switch theme happens. * * @module json-api * * @since 11.1 * * @param string $theme_slug Directory name for the theme. * @param mixed $args POST body data, including info about the theme we must switch to. */ do_action( 'jetpack_pre_switch_theme', $theme_slug, $args ); $theme = wp_get_theme( $theme_slug ); if ( ! $theme->exists() ) { return new WP_Error( 'theme_not_found', __( 'The specified theme was not found.', 'jetpack' ), 404 ); } if ( ! $theme->is_allowed() ) { return new WP_Error( 'theme_not_found', __( 'You are not allowed to switch to this theme', 'jetpack' ), 403 ); } switch_theme( $theme_slug ); return $this->get_current_theme(); } /** * Get the current theme. * * @return array */ protected function get_current_theme() { return $this->format_theme( wp_get_theme() ); } } jetpack/class.jetpack-json-api-themes-list-endpoint.php 0000644 00000001267 14722054026 0017223 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Theme list endpoint class. * * GET /sites/%s/themes */ class Jetpack_JSON_API_Themes_List_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'switch_themes'; /** * Validate the input. * * @param string $theme - the theme we're validating (unused, for keeping in sync with parent class). * * @return bool */ public function validate_input( $theme ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->themes = wp_get_themes( array( 'allowed' => true ) ); return true; } } jetpack/class.jetpack-json-api-get-database-object-backup-endpoint.php 0000644 00000010104 14722054026 0022003 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Get Database object backup endpoint class. * * /sites/%s/database-object/backup -> $blog_id */ class Jetpack_JSON_API_Get_Database_Object_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * Object type. * * @var string */ protected $object_type; /** * Object ID. * * @var int */ protected $object_id; /** * Full list of database objects that can be retrieved via this endpoint. * * @var array */ protected $object_types = array( 'woocommerce_attribute' => array( 'table' => 'woocommerce_attribute_taxonomies', 'id_field' => 'attribute_id', ), 'woocommerce_downloadable_product_permission' => array( 'table' => 'woocommerce_downloadable_product_permissions', 'id_field' => 'permission_id', ), 'woocommerce_order_item' => array( 'table' => 'woocommerce_order_items', 'id_field' => 'order_item_id', 'meta_type' => 'order_item', ), 'woocommerce_payment_token' => array( 'table' => 'woocommerce_payment_tokens', 'id_field' => 'token_id', 'meta_type' => 'payment_token', ), 'woocommerce_tax_rate' => array( 'table' => 'woocommerce_tax_rates', 'id_field' => 'tax_rate_id', 'child_table' => 'woocommerce_tax_rate_locations', 'child_id_field' => 'tax_rate_id', ), 'woocommerce_webhook' => array( 'table' => 'wc_webhooks', 'id_field' => 'webhook_id', ), ); /** * Validate input. * * @param object $object - unused. * * @return bool|WP_Error */ public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $query_args = $this->query_args(); if ( empty( $query_args['object_type'] ) || empty( $query_args['object_id'] ) ) { return new WP_Error( 'invalid_args', __( 'You must specify both an object type and id to fetch', 'jetpack' ), 400 ); } if ( empty( $this->object_types[ $query_args['object_type'] ] ) ) { return new WP_Error( 'invalid_args', __( 'Specified object_type not recognized', 'jetpack' ), 400 ); } $this->object_type = $this->object_types[ $query_args['object_type'] ]; $this->object_id = $query_args['object_id']; return true; } /** * The result. * * @return array|WP_Error */ protected function result() { global $wpdb; // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $table = $wpdb->prefix . $this->object_type['table']; $id_field = $this->object_type['id_field']; // Fetch the requested object $query = $wpdb->prepare( 'select * from `' . $table . '` where `' . $id_field . '` = %d', $this->object_id ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $object = $wpdb->get_row( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( empty( $object ) ) { return new WP_Error( 'object_not_found', __( 'Object not found', 'jetpack' ), 404 ); } $result = array( 'object' => $object ); // Fetch associated metadata (if this object type has any) if ( ! empty( $this->object_type['meta_type'] ) ) { $result['meta'] = get_metadata( $this->object_type['meta_type'], $this->object_id ); } // If there is a child linked table (eg: woocommerce_tax_rate_locations), fetch linked records if ( ! empty( $this->object_type['child_table'] ) ) { $child_table = $wpdb->prefix . $this->object_type['child_table']; $child_id_field = $this->object_type['child_id_field']; $query = $wpdb->prepare( 'select * from `' . $child_table . '` where `' . $child_id_field . '` = %d', $this->object_id ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $result['children'] = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } return $result; } } jetpack/class.jetpack-json-api-log-endpoint.php 0000644 00000001235 14722054026 0015541 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Jetpack log endpoint class. * * GET /sites/%s/jetpack-log */ class Jetpack_JSON_API_Jetpack_Log_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = 'manage_options'; /** * The result. * * @return array */ protected function result() { $args = $this->input(); $event = ( isset( $args['event'] ) && is_string( $args['event'] ) ) ? $args['event'] : false; $num = ( isset( $args['num'] ) ) ? (int) $args['num'] : false; return array( 'log' => Jetpack::get_log( $event, $num ), ); } } jetpack/class.jetpack-json-api-get-comment-backup-endpoint.php 0000644 00000003571 14722054026 0020447 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The Get comment backup endpoint class. * * /sites/%s/comments/%d/backup -> $blog_id, $comment_id */ class Jetpack_JSON_API_Get_Comment_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * The comment ID. * * @var int */ protected $comment_id; /** * Validate input * * @param int $comment_id - the comment ID. * * @return bool|WP_Error */ public function validate_input( $comment_id ) { if ( empty( $comment_id ) || ! is_numeric( $comment_id ) ) { return new WP_Error( 'comment_id_not_specified', __( 'You must specify a Comment ID', 'jetpack' ), 400 ); } $this->comment_id = (int) $comment_id; return true; } /** * The result. * * @return array|WP_Error */ protected function result() { // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $comment = get_comment( $this->comment_id ); if ( empty( $comment ) ) { return new WP_Error( 'comment_not_found', __( 'Comment not found', 'jetpack' ), 404 ); } $allowed_keys = array( 'comment_ID', 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_date', 'comment_date_gmt', 'comment_content', 'comment_karma', 'comment_approved', 'comment_agent', 'comment_type', 'comment_parent', 'user_id', ); $comment = array_intersect_key( $comment->to_array(), array_flip( $allowed_keys ) ); $comment_meta = get_comment_meta( $comment['comment_ID'] ); return array( 'comment' => $comment, 'meta' => is_array( $comment_meta ) ? $comment_meta : array(), ); } } jetpack/class.jetpack-json-api-get-user-backup-endpoint.php 0000644 00000002470 14722054026 0017760 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Get user Backup endpoint class. * * /sites/%s/users/%d/backup -> $blog_id, $user_id */ class Jetpack_JSON_API_Get_User_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * The user ID. * * @var int */ protected $user_id; /** * Validate input. * * @param int $user_id - the user ID. * * @return bool|WP_Error */ public function validate_input( $user_id ) { if ( empty( $user_id ) || ! is_numeric( $user_id ) ) { return new WP_Error( 'user_id_not_specified', __( 'You must specify a User ID', 'jetpack' ), 400 ); } $this->user_id = (int) $user_id; return true; } /** * The result. * * @return array|WP_Error */ protected function result() { // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $user = get_user_by( 'id', $this->user_id ); if ( empty( $user ) ) { return new WP_Error( 'user_not_found', __( 'User not found', 'jetpack' ), 404 ); } return array( 'user' => $user->to_array(), 'meta' => get_user_meta( $user->ID ), ); } } jetpack/class.jetpack-json-api-sync-endpoint.php 0000644 00000040454 14722054026 0015742 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Sync\Actions; use Automattic\Jetpack\Sync\Health; use Automattic\Jetpack\Sync\Modules; use Automattic\Jetpack\Sync\Queue; use Automattic\Jetpack\Sync\Queue_Buffer; use Automattic\Jetpack\Sync\Replicastore; use Automattic\Jetpack\Sync\Sender; use Automattic\Jetpack\Sync\Settings; // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound /** * Sync endpoint class. * * POST /sites/%s/sync */ class Jetpack_JSON_API_Sync_Endpoint extends Jetpack_JSON_API_Endpoint { /** * This endpoint allows authentication both via a blog and a user token. * If a user token is used, that user should have `manage_options` capability. * * @var array|string */ protected $needed_capabilities = 'manage_options'; /** * Validate the call. * * @param int $_blog_id - the blog ID. * @param string $capability - the capability. * @param bool $check_manage_active - unused. * * @return bool|WP_Error */ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable return parent::validate_call( $_blog_id, $capability, false ); } /** * The result. * * @return array */ protected function result() { $args = $this->input(); $modules = null; // convert list of modules in comma-delimited format into an array // of "$modulename => true" if ( isset( $args['modules'] ) && ! empty( $args['modules'] ) ) { $modules = array_map( '__return_true', array_flip( array_map( 'trim', explode( ',', $args['modules'] ) ) ) ); } foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) { if ( 'users' === $module_name && isset( $args[ $module_name ] ) && 'initial' === $args[ $module_name ] ) { $modules['users'] = 'initial'; } elseif ( isset( $args[ $module_name ] ) ) { $ids = explode( ',', $args[ $module_name ] ); if ( is_countable( $ids ) && count( $ids ) > 0 ) { $modules[ $module_name ] = $ids; } } } if ( empty( $modules ) ) { $modules = null; } return array( 'scheduled' => Actions::do_full_sync( $modules ) ); } /** * Validate the queue. * * @param array $query - the query. * * @return string|WP_Error */ protected function validate_queue( $query ) { if ( ! isset( $query ) ) { return new WP_Error( 'invalid_queue', 'Queue name is required', 400 ); } if ( ! in_array( $query, array( 'sync', 'full_sync', 'immediate' ), true ) ) { return new WP_Error( 'invalid_queue', 'Queue name should be sync, full_sync or immediate', 400 ); } return $query; } } /** * Sync status endpoint class. * * GET /sites/%s/sync/status */ class Jetpack_JSON_API_Sync_Status_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Callback for the endpoint. * * @return array */ protected function result() { $args = $this->query_args(); $fields = isset( $args['fields'] ) ? $args['fields'] : array(); return Actions::get_sync_status( $fields ); } } /** * Sync Check Endpoint class. * GET /sites/%s/data-check */ class Jetpack_JSON_API_Sync_Check_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Callback for the endpoint. * * @return array */ protected function result() { Actions::mark_sync_read_only(); $store = new Replicastore(); return $store->checksum_all(); } } /** * Sync histogram endpoint. * GET /sites/%s/data-histogram */ class Jetpack_JSON_API_Sync_Histogram_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Callback for endpoint. * * @return array */ protected function result() { $args = $this->query_args(); if ( isset( $args['columns'] ) ) { $columns = array_map( 'trim', explode( ',', $args['columns'] ) ); } else { $columns = null; // go with defaults } $store = new Replicastore(); if ( ! isset( $args['strip_non_ascii'] ) ) { $args['strip_non_ascii'] = true; } /** * Hack: nullify the values of `start_id` and `end_id` if we're only requesting ranges. * * The endpoint doesn't support nullable values :( */ if ( true === $args['only_range_edges'] ) { if ( 0 === $args['start_id'] ) { $args['start_id'] = null; } if ( 0 === $args['end_id'] ) { $args['end_id'] = null; } } $histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $columns, $args['strip_non_ascii'], $args['shared_salt'], $args['only_range_edges'], $args['detailed_drilldown'] ); // Hack to disable Sync during this call, so we can resolve faster. Actions::mark_sync_read_only(); return array( 'histogram' => $histogram, 'type' => $store->get_checksum_type(), ); } } /** * POST /sites/%s/sync/health */ class Jetpack_JSON_API_Sync_Modify_Health_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Callback for sync/health endpoint. * * @return array|WP_Error result of request. */ protected function result() { $args = $this->input(); switch ( $args['status'] ) { case Health::STATUS_IN_SYNC: case Health::STATUS_OUT_OF_SYNC: Health::update_status( $args['status'] ); break; default: return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' ); } // re-fetch so we see what's really being stored. return array( 'success' => Health::get_status(), ); } } /** * POST /sites/%s/sync/settings */ class Jetpack_JSON_API_Sync_Modify_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * The endpoint callback. * * @return array */ protected function result() { $args = $this->input(); $sync_settings = Settings::get_settings(); foreach ( $args as $key => $value ) { if ( $value !== false ) { if ( is_numeric( $value ) ) { $value = (int) $value; } // special case for sending empty arrays - a string with value 'empty' if ( $value === 'empty' ) { $value = array(); } $sync_settings[ $key ] = $value; } } Settings::update_settings( $sync_settings ); // re-fetch so we see what's really being stored return Settings::get_settings(); } } /** * GET /sites/%s/sync/settings */ class Jetpack_JSON_API_Sync_Get_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array */ protected function result() { return Settings::get_settings(); } } /** * GET /sites/%s/sync/object */ class Jetpack_JSON_API_Sync_Object extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { $args = $this->query_args(); $module_name = $args['module_name']; $sync_module = Modules::get_module( $module_name ); if ( ! $sync_module ) { return new WP_Error( 'invalid_module', 'You specified an invalid sync module' ); } $object_type = $args['object_type']; $object_ids = $args['object_ids']; $codec = Sender::get_instance()->get_codec(); Actions::mark_sync_read_only(); Settings::set_is_syncing( true ); $objects = $codec->encode( $sync_module->get_objects_by_id( $object_type, $object_ids ) ); Settings::set_is_syncing( false ); return array( 'objects' => $objects, 'codec' => $codec->name(), ); } } /** * Sync Now endpoint class. */ class Jetpack_JSON_API_Sync_Now_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array */ protected function result() { $args = $this->input(); $queue_name = $this->validate_queue( $args['queue'] ); if ( is_wp_error( $queue_name ) ) { return $queue_name; } $sender = Sender::get_instance(); $response = $sender->do_sync_for_queue( new Queue( $args['queue'] ) ); return array( 'response' => $response, ); } } /** * Sync checkout endpoint. */ class Jetpack_JSON_API_Sync_Checkout_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { $args = $this->input(); $queue_name = $this->validate_queue( $args['queue'] ); if ( is_wp_error( $queue_name ) ) { return $queue_name; } if ( $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) { return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and less then 100', 400 ); } $number_of_items = absint( $args['number_of_items'] ); if ( 'immediate' === $queue_name ) { return $this->immediate_full_sync_pull( $number_of_items ); } return $this->queue_pull( $queue_name, $number_of_items, $args ); } /** * Create a queue. * * @param string $queue_name - the queue name. * @param int $number_of_items - the number of items. * @param array $args - the arguments. * * @return array|WP_Error */ public function queue_pull( $queue_name, $number_of_items, $args ) { $queue = new Queue( $queue_name ); if ( 0 === $queue->size() ) { return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 ); } $sender = Sender::get_instance(); // try to give ourselves as much time as possible. set_time_limit( 0 ); if ( $args['pop'] ) { $buffer = new Queue_Buffer( 'pop', $queue->pop( $number_of_items ) ); } else { // let's delete the checkin state. if ( $args['force'] ) { $queue->unlock(); } $buffer = $this->get_buffer( $queue, $number_of_items ); } // Check that the $buffer is not checkout out already. if ( is_wp_error( $buffer ) ) { return new WP_Error( 'buffer_open', "We couldn't get the buffer it is currently checked out", 400 ); } if ( ! is_object( $buffer ) ) { return new WP_Error( 'buffer_non-object', 'Buffer is not an object', 400 ); } Settings::set_is_syncing( true ); list( $items_to_send, $skipped_items_ids ) = $sender->get_items_to_send( $buffer, $args['encode'] ); Settings::set_is_syncing( false ); return array( 'buffer_id' => $buffer->id, 'items' => $items_to_send, 'skipped_items' => $skipped_items_ids, 'codec' => $args['encode'] ? $sender->get_codec()->name() : null, 'sent_timestamp' => time(), ); } /** * The items. * * @var array */ public $items = array(); /** * Send the data listener. */ public function jetpack_sync_send_data_listener() { foreach ( func_get_args()[0] as $key => $item ) { $this->items[ $key ] = $item; } } /** * Check out a buffer of full sync actions. * * @param null $number_of_items Number of Actions to check-out. * * @return array Sync Actions to be returned to requestor */ public function immediate_full_sync_pull( $number_of_items = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable // try to give ourselves as much time as possible. set_time_limit( 0 ); $original_send_data_cb = array( 'Automattic\Jetpack\Sync\Actions', 'send_data' ); $temp_send_data_cb = array( $this, 'jetpack_sync_send_data_listener' ); Sender::get_instance()->set_enqueue_wait_time( 0 ); remove_filter( 'jetpack_sync_send_data', $original_send_data_cb ); add_filter( 'jetpack_sync_send_data', $temp_send_data_cb, 10, 6 ); Sender::get_instance()->do_full_sync(); remove_filter( 'jetpack_sync_send_data', $temp_send_data_cb ); add_filter( 'jetpack_sync_send_data', $original_send_data_cb, 10, 6 ); return array( 'items' => $this->items, 'codec' => Sender::get_instance()->get_codec()->name(), 'sent_timestamp' => time(), 'status' => Actions::get_sync_status(), ); } /** * Get the queue buffer. * * @param object $queue - the queue. * @param int $number_of_items - the number of items. * * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error */ protected function get_buffer( $queue, $number_of_items ) { $start = time(); $max_duration = 5; // this will try to get the buffer $buffer = $queue->checkout( $number_of_items ); $duration = time() - $start; while ( is_wp_error( $buffer ) && $duration < $max_duration ) { sleep( 2 ); $duration = time() - $start; $buffer = $queue->checkout( $number_of_items ); } if ( $buffer === false ) { return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 ); } return $buffer; } } /** * Close endpoint class. */ class Jetpack_JSON_API_Sync_Close_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { $request_body = $this->input(); $queue_name = $this->validate_queue( $request_body['queue'] ); if ( is_wp_error( $queue_name ) ) { return $queue_name; } if ( ! isset( $request_body['buffer_id'] ) ) { return new WP_Error( 'missing_buffer_id', 'Please provide a buffer id', 400 ); } if ( ! isset( $request_body['item_ids'] ) || ! is_array( $request_body['item_ids'] ) ) { return new WP_Error( 'missing_item_ids', 'Please provide a list of item ids in the item_ids argument', 400 ); } $request_body['buffer_id'] = preg_replace( '/[^A-Za-z0-9]/', '', $request_body['buffer_id'] ); $request_body['item_ids'] = array_filter( array_map( array( 'Jetpack_JSON_API_Sync_Close_Endpoint', 'sanitize_item_ids' ), $request_body['item_ids'] ) ); $queue = new Queue( $queue_name ); $items = $queue->peek_by_id( $request_body['item_ids'] ); // Update Full Sync Status if queue is "full_sync". if ( 'full_sync' === $queue_name ) { $full_sync_module = Modules::get_module( 'full-sync' ); '@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $full_sync_module'; $full_sync_module->update_sent_progress_action( $items ); } $buffer = new Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] ); $response = $queue->close( $buffer, $request_body['item_ids'] ); // Perform another checkout? if ( isset( $request_body['continue'] ) && $request_body['continue'] ) { if ( in_array( $queue_name, array( 'full_sync', 'immediate' ), true ) ) { // Send Full Sync Actions. Sender::get_instance()->do_full_sync(); } elseif ( $queue->has_any_items() ) { // Send Incremental Sync Actions. Sender::get_instance()->do_sync(); } } if ( is_wp_error( $response ) ) { return $response; } return array( 'success' => $response, 'status' => Actions::get_sync_status(), ); } /** * Sanitize item IDs. * * @param string $item - the item we're sanitizing. * * @return string|null */ protected static function sanitize_item_ids( $item ) { // lets not delete any options that don't start with jpsq_sync- if ( ! is_string( $item ) || ! str_starts_with( $item, 'jpsq_' ) ) { return null; } // Limit to A-Z,a-z,0-9,_,-,. return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item ); } } /** * Unlock ednpoint class. */ class Jetpack_JSON_API_Sync_Unlock_Endpoint extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { $args = $this->input(); if ( ! isset( $args['queue'] ) ) { return new WP_Error( 'invalid_queue', 'Queue name is required', 400 ); } if ( ! in_array( $args['queue'], array( 'sync', 'full_sync' ), true ) ) { return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 ); } $queue = new Queue( $args['queue'] ); // False means that there was no lock to delete. $response = $queue->unlock(); return array( 'success' => $response, ); } } /** * Object ID range class. */ class Jetpack_JSON_API_Sync_Object_Id_Range extends Jetpack_JSON_API_Sync_Endpoint { /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { $args = $this->query_args(); $module_name = $args['sync_module']; $batch_size = $args['batch_size']; if ( ! $this->is_valid_sync_module( $module_name ) ) { return new WP_Error( 'invalid_module', 'This sync module cannot be used to calculate a range.', 400 ); } $module = Modules::get_module( $module_name ); return array( 'ranges' => $module->get_min_max_object_ids_for_batches( $batch_size ), ); } /** * Check if sync module is valid. * * @param string $module_name - the module name. * * @return bool */ protected function is_valid_sync_module( $module_name ) { return in_array( $module_name, array( 'comments', 'posts', 'terms', 'term_relationships', 'users', ), true ); } } jetpack/class.jetpack-json-api-user-connect-endpoint.php 0000644 00000003130 14722054026 0017361 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Connection\Tokens; /** * User connect endpoint class. */ class Jetpack_JSON_API_User_Connect_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'create_users'; /** * The user ID. * * @var int */ private $user_id; /** * The user token. * * @var string */ private $user_token; /** * The endpoint callback. * * @return array */ public function result() { ( new Tokens() )->update_user_token( $this->user_id, sprintf( '%s.%d', $this->user_token, $this->user_id ), false ); return array( 'success' => ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $this->user_id ) ); } /** * Validate input. * * @param int $user_id - the User ID. * * @return bool|WP_Error */ public function validate_input( $user_id ) { $input = $this->input(); if ( ! isset( $user_id ) ) { return new WP_Error( 'input_error', __( 'user_id is required', 'jetpack' ) ); } $this->user_id = $user_id; if ( ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $this->user_id ) ) { return new WP_Error( 'user_already_connected', __( 'The user is already connected', 'jetpack' ) ); } if ( ! isset( $input['user_token'] ) ) { return new WP_Error( 'input_error', __( 'user_token is required', 'jetpack' ) ); } $this->user_token = sanitize_text_field( $input['user_token'] ); return parent::validate_input( $user_id ); } } jetpack/class.jetpack-json-api-modules-endpoint.php 0000644 00000011170 14722054026 0016427 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Base class for working with Jetpack Modules. */ abstract class Jetpack_JSON_API_Modules_Endpoint extends Jetpack_JSON_API_Endpoint { /** * The modules. * * @var array */ protected $modules = array(); /** * If we're working in bulk. * * @var boolean */ protected $bulk = true; /** * Response format. * * @var array */ public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore 'id' => '(string) The module\'s ID', 'active' => '(boolean) The module\'s status.', 'name' => '(string) The module\'s name.', 'description' => '(safehtml) The module\'s description.', 'sort' => '(int) The module\'s display order.', 'introduced' => '(string) The Jetpack version when the module was introduced.', 'changed' => '(string) The Jetpack version when the module was changed.', 'free' => '(boolean) The module\'s Free or Paid status.', 'module_tags' => '(array) The module\'s tags.', 'override' => '(string) The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'', ); /** * The result. * * @return array */ protected function result() { $modules = $this->get_modules(); if ( ! $this->bulk && ! empty( $modules ) ) { return array_pop( $modules ); } return array( 'modules' => $modules ); } /** * Walks through either the submitted modules or list of themes and creates the global array. * * @param string $module - the modules. * * @return bool|WP_Error */ protected function validate_input( $module ) { $args = $this->input(); // lets set what modules were requested, and validate them if ( ! isset( $module ) || empty( $module ) ) { if ( ! $args['modules'] || empty( $args['modules'] ) ) { return new WP_Error( 'missing_module', __( 'You are required to specify a module.', 'jetpack' ), 400 ); } if ( is_array( $args['modules'] ) ) { $this->modules = $args['modules']; } else { $this->modules[] = $args['modules']; } } else { $this->modules[] = urldecode( $module ); $this->bulk = false; } $error = $this->validate_modules(); if ( is_wp_error( $error ) ) { return $error; } return parent::validate_input( $module ); } /** * Walks through submitted themes to make sure they are valid * * @return bool|WP_Error */ protected function validate_modules() { foreach ( $this->modules as $module ) { if ( ! Jetpack::is_module( $module ) ) { // Translators: the module that's not found. return new WP_Error( 'unknown_jetpack_module', sprintf( __( 'Module not found: `%s`.', 'jetpack' ), $module ), 404 ); } } return true; } /** * Format the module. * * @param string $module_slug - the module slug. * * @return array */ protected static function format_module( $module_slug ) { $module_data = Jetpack::get_module( $module_slug ); $module = array(); $module['id'] = $module_slug; $module['active'] = Jetpack::is_module_active( $module_slug ); $module['name'] = $module_data['name']; $module['short_description'] = $module_data['description']; $module['sort'] = $module_data['sort']; $module['introduced'] = $module_data['introduced']; $module['changed'] = $module_data['changed']; $module['free'] = $module_data['free']; $module['module_tags'] = array_map( 'jetpack_get_module_i18n_tag', $module_data['module_tags'] ); $overrides_instance = Jetpack_Modules_Overrides::instance(); $module['override'] = $overrides_instance->get_module_override( $module_slug ); // Fetch the HTML formatted long description ob_start(); /** This action is documented in class.jetpack-modules-list-table.php */ do_action( 'jetpack_module_more_info_' . $module_slug ); $module['description'] = ob_get_clean(); return $module; } /** * Format a list of modules for public display, using the supplied offset and limit args * * @uses WPCOM_JSON_API_Endpoint::query_args() * @return array Public API modules objects */ protected function get_modules() { $modules = array_values( $this->modules ); // do offset & limit - we've already returned a 400 error if they're bad numbers $args = $this->query_args(); if ( isset( $args['offset'] ) ) { $modules = array_slice( $modules, (int) $args['offset'] ); } if ( isset( $args['limit'] ) ) { $modules = array_slice( $modules, 0, (int) $args['limit'] ); } return array_map( array( $this, 'format_module' ), $modules ); } } jetpack/class.jetpack-json-api-modules-list-endpoint.php 0000644 00000001201 14722054026 0017372 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileNames /** * Modules list endpoint. * * GET /sites/%s/jetpack/modules */ class Jetpack_JSON_API_Modules_List_Endpoint extends Jetpack_JSON_API_Modules_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'jetpack_manage_modules'; /** * Validate the input. * * @param string $module - the module. * * @return bool */ public function validate_input( $module ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->modules = Jetpack::get_available_modules(); return true; } } jetpack/class.wpcom-json-api-get-option-endpoint.php 0000644 00000003765 14722054026 0016563 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Sync\Defaults; /** * Get option endpoint. */ class WPCOM_JSON_API_Get_Option_Endpoint extends Jetpack_JSON_API_Endpoint { /** * This endpoint allows authentication both via a blog and a user token. * If a user token is used, that user should have `manage_options` capability. * * @var array|string */ protected $needed_capabilities = 'manage_options'; /** * Options name. * * @var string */ public $option_name; /** * Site option. * * @var string */ public $site_option; /** * Endpoint callback. * * @return array */ public function result() { if ( $this->site_option ) { return array( 'option_value' => get_site_option( $this->option_name ) ); } return array( 'option_value' => get_option( $this->option_name ) ); } /** * Validate the input. * * @param object $object - unused, for parent class compatability. * * @return bool|WP_Error */ public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $query_args = $this->query_args(); $this->option_name = isset( $query_args['option_name'] ) ? $query_args['option_name'] : false; if ( ! $this->option_name ) { return new WP_Error( 'option_name_not_set', __( 'You must specify an option_name', 'jetpack' ) ); } $this->site_option = isset( $query_args['site_option'] ) ? $query_args['site_option'] : false; /** * Filter the list of options that are manageable via the JSON API. * * @module json-api * * @since 3.8.2 * * @param array The default list of site options. * @param bool Is the option a site option. */ if ( ! in_array( $this->option_name, apply_filters( 'jetpack_options_whitelist', Defaults::$default_options_whitelist, $this->site_option ), true ) ) { return new WP_Error( 'option_name_not_in_whitelist', __( 'You must specify a whitelisted option_name', 'jetpack' ) ); } return true; } } jetpack/class.jetpack-json-api-check-capabilities-endpoint.php 0000644 00000002431 14722054026 0020463 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Check capabilities endpoint class. * * GET /sites/%s/me/capability */ class Jetpack_JSON_API_Check_Capabilities_Endpoint extends Jetpack_JSON_API_Modules_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $_blog_id - the blog ID. * @param object $object - parameter is for making the method signature compatible with its parent class method. * @return bool|bool[]|WP_Error */ public function callback( $path = '', $_blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable // Check minimum capability and blog membership first $error = $this->validate_call( $_blog_id, 'read', false ); if ( is_wp_error( $error ) ) { return $error; } $args = $this->input(); if ( ! isset( $args['capability'] ) || empty( $args['capability'] ) ) { return new WP_Error( 'missing_capability', __( 'You are required to specify a capability to check.', 'jetpack' ), 400 ); } $capability = $args['capability']; if ( is_array( $capability ) ) { $results = array_map( 'current_user_can', $capability ); return array_combine( $capability, $results ); } else { return current_user_can( $capability ); } } } jetpack/class.jetpack-json-api-themes-new-endpoint.php 0000644 00000006067 14722054026 0017044 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; use Automattic\Jetpack\Automatic_Install_Skin; /** * Themes new endpoint class. * * /sites/%s/themes/%s/install */ class Jetpack_JSON_API_Themes_New_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'install_themes'; /** * Action. * * @var string */ protected $action = 'install'; /** * Download links. * * @var array */ protected $download_links = array(); /** * Validate the call. * * @param int $_blog_id - the blod ID. * @param string $capability - the capability we're checking. * @param bool $check_manage_active - if managing capabilities is active. * * @return bool|WP_Error */ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { $validate = parent::validate_call( $_blog_id, $capability, $check_manage_active ); if ( is_wp_error( $validate ) ) { // Lets delete the attachment... if the user doesn't have the right permissions to do things. $args = $this->input(); if ( isset( $args['zip'][0]['id'] ) ) { wp_delete_attachment( $args['zip'][0]['id'], true ); } } return $validate; } /** * Validate the input. * * @param string $theme - the theme. */ protected function validate_input( $theme ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->bulk = false; $this->themes = array(); } /** * Install the theme. * * @return bool */ public function install() { $args = $this->input(); if ( isset( $args['zip'][0]['id'] ) ) { $attachment_id = $args['zip'][0]['id']; $local_file = get_attached_file( $attachment_id ); if ( ! $local_file ) { return new WP_Error( 'local-file-does-not-exist' ); } $skin = new Automatic_Install_Skin(); $upgrader = new Theme_Upgrader( $skin ); $pre_install_list = wp_get_themes(); $result = $upgrader->install( $local_file ); // clean up. wp_delete_attachment( $attachment_id, true ); if ( is_wp_error( $result ) ) { return $result; } $after_install_list = wp_get_themes(); $plugin = array_values( array_diff( array_keys( $after_install_list ), array_keys( $pre_install_list ) ) ); if ( ! $result ) { $error_code = $skin->get_main_error_code(); $message = $skin->get_main_error_message(); if ( empty( $message ) ) { $message = __( 'An unknown error occurred during installation', 'jetpack' ); } if ( 'download_failed' === $error_code ) { $error_code = 'no_package'; } return new WP_Error( $error_code, $message, 400 ); } if ( empty( $plugin ) ) { return new WP_Error( 'theme_already_installed' ); } $this->themes = $plugin; $this->log[ $plugin[0] ] = $upgrader->skin->get_upgrade_messages(); return true; } return new WP_Error( 'no_theme_installed' ); } } jetpack/class.jetpack-json-api-themes-modify-endpoint.php 0000644 00000011774 14722054026 0017543 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Themes modify endpoint class. * POST /sites/%s/themes/%s * POST /sites/%s/themes */ class Jetpack_JSON_API_Themes_Modify_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'update_themes'; /** * The action. * * @var string */ protected $action = 'default_action'; /** * Expected actions. * * @var array */ protected $expected_actions = array( 'update', 'update_translations' ); /** * The default action. * * @return bool */ public function default_action() { $args = $this->input(); if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) { if ( $args['autoupdate'] ) { $this->autoupdate_on(); } else { $this->autoupdate_off(); } } if ( isset( $args['autoupdate_translations'] ) && is_bool( $args['autoupdate_translations'] ) ) { if ( $args['autoupdate_translations'] ) { $this->autoupdate_translations_on(); } else { $this->autoupdate_translations_off(); } } return true; } /** * Turn autoupdate on. */ public function autoupdate_on() { $autoupdate_themes = Jetpack_Options::get_option( 'autoupdate_themes', array() ); $autoupdate_themes = array_unique( array_merge( $autoupdate_themes, $this->themes ) ); Jetpack_Options::update_option( 'autoupdate_themes', $autoupdate_themes ); } /** * Turn autoupdate off. */ public function autoupdate_off() { $autoupdate_themes = Jetpack_Options::get_option( 'autoupdate_themes', array() ); $autoupdate_themes = array_diff( $autoupdate_themes, $this->themes ); Jetpack_Options::update_option( 'autoupdate_themes', $autoupdate_themes ); } /** * Autoupdate translations on. */ public function autoupdate_translations_on() { $autoupdate_themes_translations = Jetpack_Options::get_option( 'autoupdate_themes_translations', array() ); $autoupdate_themes_translations = array_unique( array_merge( $autoupdate_themes_translations, $this->themes ) ); Jetpack_Options::update_option( 'autoupdate_themes_translations', $autoupdate_themes_translations ); } /** * Autoupdate translations off. */ public function autoupdate_translations_off() { $autoupdate_themes_translations = Jetpack_Options::get_option( 'autoupdate_themes_translations', array() ); $autoupdate_themes_translations = array_diff( $autoupdate_themes_translations, $this->themes ); Jetpack_Options::update_option( 'autoupdate_themes_translations', $autoupdate_themes_translations ); } /** * Update the theme. * * @return bool|WP_Error True on success, WP_Error on failure. */ public function update() { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // Clear the cache. wp_update_themes(); $result = null; foreach ( $this->themes as $theme ) { /** * Pre-upgrade action * * @since 3.9.3 * * @param object $theme WP_Theme object * @param array $themes Array of theme objects */ do_action( 'jetpack_pre_theme_upgrade', $theme, $this->themes ); // Objects created inside the for loop to clean the messages for each theme $skin = new Automatic_Upgrader_Skin(); $upgrader = new Theme_Upgrader( $skin ); $upgrader->init(); $result = $upgrader->upgrade( $theme ); $this->log[ $theme ][] = $upgrader->skin->get_upgrade_messages(); } if ( ! $this->bulk && ! $result ) { return new WP_Error( 'update_fail', __( 'There was an error updating your theme', 'jetpack' ), 400 ); } return true; } /** * Update translations. * * @return bool|WP_Error */ public function update_translations() { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // Clear the cache. wp_update_themes(); $available_themes_updates = get_site_transient( 'update_themes' ); if ( ! isset( $available_themes_updates->translations ) || empty( $available_themes_updates->translations ) ) { return new WP_Error( 'nothing_to_translate' ); } $result = null; foreach ( $available_themes_updates->translations as $translation ) { $theme = $translation['slug']; if ( ! in_array( $translation['slug'], $this->themes, true ) ) { $this->log[ $theme ][] = __( 'No update needed', 'jetpack' ); continue; } /** * Pre-upgrade action * * @since 4.4.0 * * @param object $theme WP_Theme object * @param array $themes Array of theme objects */ do_action( 'jetpack_pre_theme_upgrade_translations', $theme, $this->themes ); // Objects created inside the for loop to clean the messages for each theme $skin = new Automatic_Upgrader_Skin(); $upgrader = new Language_Pack_Upgrader( $skin ); $upgrader->init(); $result = $upgrader->upgrade( (object) $translation ); $this->log[ $theme ] = $upgrader->skin->get_upgrade_messages(); } if ( ! $this->bulk && ! $result ) { return new WP_Error( 'update_fail', __( 'There was an error updating your theme', 'jetpack' ), 400 ); } return true; } } jetpack/class.jetpack-json-api-plugins-modify-endpoint.php 0000644 00000043713 14722054026 0017735 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Constants; new Jetpack_JSON_API_Plugins_Modify_Endpoint( array( 'description' => 'Activate/Deactivate a Plugin on your Jetpack Site, or set automatic updates', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s', 'stat' => 'plugins:1:modify', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(string) The plugin ID', ), 'allow_jetpack_site_auth' => true, 'request_format' => array( 'action' => '(string) Possible values are \'update\'', 'autoupdate' => '(bool) Whether or not to automatically update the plugin', 'active' => '(bool) Activate or deactivate the plugin', 'network_wide' => '(bool) Do action network wide (default value: false)', 'scheduled_update' => '(bool) If the update is happening as a result of a scheduled update event', ), 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'action' => 'update', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello', ) ); new Jetpack_JSON_API_Plugins_Modify_Endpoint( array( 'description' => 'Activate/Deactivate a list of plugins on your Jetpack Site, or set automatic updates', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins', 'stat' => 'plugins:modify', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'action' => '(string) Possible values are \'update\'', 'autoupdate' => '(bool) Whether or not to automatically update the plugin', 'active' => '(bool) Activate or deactivate the plugin', 'network_wide' => '(bool) Do action network wide (default value: false)', 'plugins' => '(array) A list of plugin ids to modify', ), 'allow_jetpack_site_auth' => true, 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => array( 'plugins' => '(array:plugin) An array of plugin objects.', 'updated' => '(array) A list of plugin ids that were updated. Only present if action is update.', 'not_updated' => '(array) A list of plugin ids that were not updated. Only present if action is update.', 'log' => '(array) Update log. Only present if action is update.', ), 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'active' => true, 'plugins' => array( 'jetpack/jetpack', 'akismet/akismet', ), ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins', ) ); new Jetpack_JSON_API_Plugins_Modify_Endpoint( array( 'description' => 'Update a Plugin on your Jetpack Site', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/update/', 'stat' => 'plugins:1:update', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(string) The plugin ID', ), 'allow_jetpack_site_auth' => true, 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello/update', ) ); /** * Plugins modify endpoint class. * * POST /sites/%s/plugins/%s * POST /sites/%s/plugins */ class Jetpack_JSON_API_Plugins_Modify_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * The slug. * * @var string */ protected $slug = null; /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'activate_plugins'; /** * Action. * * @var string */ protected $action = 'default_action'; /** * Expected actions. * * @var array */ protected $expected_actions = array( 'update', 'install', 'delete', 'update_translations' ); /** * Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param object $object - the object. * * @return bool|WP_Error */ public function callback( $path = '', $blog_id = 0, $object = null ) { Jetpack_JSON_API_Endpoint::validate_input( $object ); switch ( $this->action ) { case 'delete': $this->needed_capabilities = 'delete_plugins'; break; case 'update_translations': case 'update': $this->needed_capabilities = 'update_plugins'; break; case 'install': $this->needed_capabilities = 'install_plugins'; break; } $args = $this->input(); if ( is_array( $args ) && ( isset( $args['autoupdate'] ) || isset( $args['autoupdate_translations'] ) ) ) { $this->needed_capabilities = 'update_plugins'; } return parent::callback( $path, $blog_id, $object ); } /** * The default action. * * @return bool|WP_Error */ public function default_action() { $args = $this->input(); if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) { if ( $args['autoupdate'] ) { $this->autoupdate_on(); } else { $this->autoupdate_off(); } } if ( isset( $args['active'] ) && is_bool( $args['active'] ) ) { if ( $args['active'] ) { // We don't have to check for activate_plugins permissions since we assume that the user has those // Since we set them via $needed_capabilities. return $this->activate(); } elseif ( $this->current_user_can( 'deactivate_plugins' ) ) { return $this->deactivate(); } else { return new WP_Error( 'unauthorized_error', __( 'Plugin deactivation is not allowed', 'jetpack' ), '403' ); } } if ( isset( $args['autoupdate_translations'] ) && is_bool( $args['autoupdate_translations'] ) ) { if ( $args['autoupdate_translations'] ) { $this->autoupdate_translations_on(); } else { $this->autoupdate_translations_off(); } } return true; } /** * Turn on autoupdate. */ protected function autoupdate_on() { $autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() ); $autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) ); update_site_option( 'auto_update_plugins', $autoupdate_plugins ); } /** * Turn off autoupdate. */ protected function autoupdate_off() { $autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() ); $autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins ); update_site_option( 'auto_update_plugins', $autoupdate_plugins ); } /** * Turn autoupdate translations on. */ protected function autoupdate_translations_on() { $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ); $autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) ); Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins ); } /** * Turn autoupdate translations off. */ protected function autoupdate_translations_off() { $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ); $autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins ); Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins ); } /** * Activate the plugin. * * @return null|WP_Error null if the activation was successful. */ protected function activate() { $permission_error = false; foreach ( $this->plugins as $plugin ) { if ( ! $this->current_user_can( 'activate_plugin', $plugin ) ) { $this->log[ $plugin ]['error'] = __( 'Sorry, you are not allowed to activate this plugin.', 'jetpack' ); $has_errors = true; $permission_error = true; continue; } if ( ( ! $this->network_wide && Jetpack::is_plugin_active( $plugin ) ) || is_plugin_active_for_network( $plugin ) ) { $this->log[ $plugin ]['error'] = __( 'The Plugin is already active.', 'jetpack' ); $has_errors = true; continue; } if ( ! $this->network_wide && is_network_only_plugin( $plugin ) && is_multisite() ) { $this->log[ $plugin ]['error'] = __( 'Plugin can only be Network Activated', 'jetpack' ); $has_errors = true; continue; } $result = activate_plugin( $plugin, '', $this->network_wide ); if ( is_wp_error( $result ) ) { $this->log[ $plugin ]['error'] = $result->get_error_messages(); $has_errors = true; continue; } $success = Jetpack::is_plugin_active( $plugin ); if ( $success && $this->network_wide ) { $success &= is_plugin_active_for_network( $plugin ); } if ( ! $success ) { $this->log[ $plugin ]['error'] = $result->get_error_messages; $has_errors = true; continue; } $this->log[ $plugin ][] = __( 'Plugin activated.', 'jetpack' ); } if ( ! $this->bulk && isset( $has_errors ) ) { $plugin = $this->plugins[0]; if ( $permission_error ) { return new WP_Error( 'unauthorized_error', $this->log[ $plugin ]['error'], 403 ); } return new WP_Error( 'activation_error', $this->log[ $plugin ]['error'] ); } } /** * Check if the current user has capabilities. * * @param string $capability - the capability we're checking. * @param string $plugin - the plugin we're checking. * * @return bool */ protected function current_user_can( $capability, $plugin = null ) { // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check. if ( $this->accepts_site_based_authentication() ) { return true; } if ( $plugin ) { return current_user_can( $capability, $plugin ); } return current_user_can( $capability ); } /** * Deactivate the plugin. * * @return null|WP_Error null if the deactivation was successful */ protected function deactivate() { $permission_error = false; foreach ( $this->plugins as $plugin ) { if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) { $error = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' ); $this->log[ $plugin ]['error'] = $error; $permission_error = true; continue; } if ( ! Jetpack::is_plugin_active( $plugin ) ) { $error = __( 'The Plugin is already deactivated.', 'jetpack' ); $this->log[ $plugin ]['error'] = $error; continue; } deactivate_plugins( $plugin, false, $this->network_wide ); $success = ! Jetpack::is_plugin_active( $plugin ); if ( $success && $this->network_wide ) { $success &= ! is_plugin_active_for_network( $plugin ); } if ( ! $success ) { $error = __( 'There was an error deactivating your plugin', 'jetpack' ); $this->log[ $plugin ]['error'] = $error; continue; } $this->log[ $plugin ][] = __( 'Plugin deactivated.', 'jetpack' ); } if ( ! $this->bulk && isset( $error ) ) { if ( $permission_error ) { return new WP_Error( 'unauthorized_error', $error, 403 ); } return new WP_Error( 'deactivation_error', $error ); } } /** * Update the plugin. * * @return bool|WP_Error */ protected function update() { $query_args = $this->query_args(); if ( isset( $query_args['autoupdate'] ) && $query_args['autoupdate'] || $this->scheduled_update ) { Constants::set_constant( 'JETPACK_PLUGIN_AUTOUPDATE', true ); } if ( $this->scheduled_update ) { Constants::set_constant( 'SCHEDULED_AUTOUPDATE', true ); } wp_clean_plugins_cache( false ); ob_start(); wp_update_plugins(); // Check for Plugin updates ob_end_clean(); $update_plugins = get_site_transient( 'update_plugins' ); if ( isset( $update_plugins->response ) ) { $plugin_updates_needed = array_keys( $update_plugins->response ); } else { $plugin_updates_needed = array(); } $update_attempted = false; include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // unhook this functions that output things before we send our response header. remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); remove_action( 'upgrader_process_complete', 'wp_version_check' ); remove_action( 'upgrader_process_complete', 'wp_update_themes' ); // Set the lock timeout to 15 minutes if it's scheduled update, otherwise default to one hour. $lock_release_timeout = $this->scheduled_update ? 15 * MINUTE_IN_SECONDS : null; // Early return if unable to obtain auto_updater lock. // @see https://github.com/WordPress/wordpress-develop/blob/66469efa99e7978c8824e287834135aa9842e84f/src/wp-admin/includes/class-wp-automatic-updater.php#L453. if ( Constants::get_constant( 'JETPACK_PLUGIN_AUTOUPDATE' ) && ! WP_Upgrader::create_lock( 'auto_updater', $lock_release_timeout ) ) { return new WP_Error( 'update_fail', __( 'Updates are already in progress.', 'jetpack' ), 400 ); } $result = false; foreach ( $this->plugins as $plugin ) { if ( ! in_array( $plugin, $plugin_updates_needed, true ) ) { $this->log[ $plugin ][] = __( 'No update needed', 'jetpack' ); continue; } // Rely on WP_Automatic_Updater class to check if a plugin item should be updated if it is a Jetpack autoupdate request. if ( Constants::get_constant( 'JETPACK_PLUGIN_AUTOUPDATE' ) && ! ( new WP_Automatic_Updater() )->should_update( 'plugin', $update_plugins->response[ $plugin ], WP_PLUGIN_DIR ) ) { continue; } // Establish per plugin lock. $plugin_slug = Jetpack_Autoupdate::get_plugin_slug( $plugin ); if ( ! WP_Upgrader::create_lock( 'jetpack_' . $plugin_slug, $lock_release_timeout ) ) { continue; } /** * Pre-upgrade action * * @since 3.9.3 * * @param array $plugin Plugin data * @param array $plugin Array of plugin objects * @param bool $updated_attempted false for the first update, true subsequently */ do_action( 'jetpack_pre_plugin_upgrade', $plugin, $this->plugins, $update_attempted ); $update_attempted = true; // Object created inside the for loop to clean the messages for each plugin $skin = new WP_Ajax_Upgrader_Skin(); // The Automatic_Upgrader_Skin skin shouldn't output anything. $upgrader = new Plugin_Upgrader( $skin ); $upgrader->init(); // This avoids the plugin to be deactivated. // Using bulk upgrade puts the site into maintenance mode during the upgrades $result = $upgrader->bulk_upgrade( array( $plugin ) ); $errors = $skin->get_errors(); $this->log[ $plugin ] = $skin->get_upgrade_messages(); // release individual plugin lock. WP_Upgrader::release_lock( 'jetpack_' . $plugin_slug ); if ( is_wp_error( $errors ) && $errors->get_error_code() ) { return $errors; } } // release auto_udpate lock. if ( Constants::get_constant( 'JETPACK_PLUGIN_AUTOUPDATE' ) ) { WP_Upgrader::release_lock( 'auto_updater' ); } if ( ! $this->bulk && ! $result && $update_attempted ) { return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 ); } return $this->default_action(); } /** * Update translations. * * @return bool|WP_Error */ public function update_translations() { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // Clear the cache. wp_clean_plugins_cache( false ); ob_start(); wp_update_plugins(); // Check for Plugin updates ob_end_clean(); $available_updates = get_site_transient( 'update_plugins' ); if ( ! isset( $available_updates->translations ) || empty( $available_updates->translations ) ) { return new WP_Error( 'nothing_to_translate' ); } $update_attempted = false; $result = false; foreach ( $this->plugins as $plugin ) { $this->slug = Jetpack_Autoupdate::get_plugin_slug( $plugin ); $translation = array_filter( $available_updates->translations, array( $this, 'get_translation' ) ); if ( empty( $translation ) ) { $this->log[ $plugin ][] = __( 'No update needed', 'jetpack' ); continue; } /** * Pre-upgrade action * * @since 4.4.0 * * @param array $plugin Plugin data * @param array $plugin Array of plugin objects * @param bool $update_attempted false for the first update, true subsequently */ do_action( 'jetpack_pre_plugin_upgrade_translations', $plugin, $this->plugins, $update_attempted ); $update_attempted = true; $skin = new Automatic_Upgrader_Skin(); $upgrader = new Language_Pack_Upgrader( $skin ); $upgrader->init(); $result = $upgrader->upgrade( (object) $translation[0] ); $this->log[ $plugin ] = $upgrader->skin->get_upgrade_messages(); } if ( ! $this->bulk && ! $result ) { return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 ); } return true; } /** * Test whether the translation matches `$this->slug`. * * @param array $translation - the translation. * * @return bool */ protected function get_translation( $translation ) { return ( $translation['slug'] === $this->slug ); } } jetpack/class.jetpack-json-api-endpoint.php 0000644 00000011740 14722054026 0014764 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require JETPACK__PLUGIN_DIR . '/modules/module-info.php'; /** * Base class for Jetpack Endpoints, has the validate_call helper function. */ abstract class Jetpack_JSON_API_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities; /** * Expected actions. * * @var array */ protected $expected_actions = array(); /** * The action. * * @var string */ protected $action; /** * Callback function. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param object $object - parameter is for making the method signature compatible with its parent class method. */ public function callback( $path = '', $blog_id = 0, $object = null ) { $error = $this->validate_call( $blog_id, $this->needed_capabilities ); if ( is_wp_error( $error ) ) { return $error; } $error = $this->validate_input( $object ); if ( is_wp_error( $error ) ) { return $error; } if ( ! empty( $this->action ) ) { $error = call_user_func( array( $this, $this->action ) ); if ( is_wp_error( $error ) ) { return $error; } } return $this->result(); } /** * The result function. */ abstract protected function result(); /** * Validate input. * * @param object $object - unused, for parent class compatability. * * @return bool */ protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $args = $this->input(); if ( isset( $args['action'] ) && $args['action'] === 'update' ) { $this->action = 'update'; } if ( preg_match( '!/update/?$!', $this->path ) ) { $this->action = 'update'; } elseif ( preg_match( '/\/install\/?$/', $this->path ) ) { $this->action = 'install'; } elseif ( ! empty( $args['action'] ) ) { if ( ! in_array( $args['action'], $this->expected_actions, true ) ) { return new WP_Error( 'invalid_action', __( 'You must specify a valid action', 'jetpack' ) ); } $this->action = $args['action']; } return true; } /** * Switches to the blog and checks current user capabilities. * * @param int $_blog_id - the blog ID. * @param array $capability - the capabilities of the user. * @param bool $check_validation - if we're checking the validation. * * @return bool|WP_Error a WP_Error object or true if things are good. */ protected function validate_call( $_blog_id, $capability, $check_validation = true ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $_blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $error = $this->check_capability( $capability ); if ( is_wp_error( $error ) ) { return $error; } if ( $check_validation && 'GET' !== $this->method && /** * Filter to disallow JSON API requests to the site. * Setting to false disallows you to manage your site remotely from WordPress.com * and disallows plugin auto-updates. * * @since 7.3.0 * * @param bool $check_validation Whether to allow API requests to manage the site */ ! apply_filters( 'jetpack_json_manage_api_enabled', $check_validation ) ) { return new WP_Error( 'unauthorized_full_access', __( 'Full management mode is off for this site.', 'jetpack' ), 403 ); } return true; } /** * Check capability. * * @param array $capability - the compatability. * * @return bool|WP_Error */ protected function check_capability( $capability ) { // If this endpoint accepts site based authentication, skip capabilities check. if ( $this->accepts_site_based_authentication() ) { return true; } if ( is_array( $capability ) ) { // the idea is that the we can pass in an array of capabilitie that the user needs to have before we allowing them to do something $capabilities = ( isset( $capability['capabilities'] ) ? $capability['capabilities'] : $capability ); // We can pass in the number of conditions we must pass by default it is all. $must_pass = ( isset( $capability['must_pass'] ) && is_int( $capability['must_pass'] ) ? $capability['must_pass'] : count( $capabilities ) ); $failed = array(); // store the failed capabilities $passed = 0; foreach ( $capabilities as $cap ) { if ( current_user_can( $cap ) ) { ++$passed; } else { $failed[] = $cap; } } // Check if all conditions have passed. if ( $passed < $must_pass ) { return new WP_Error( 'unauthorized', /* translators: %s: comma-separated list of capabilities */ sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), implode( ', ', $failed ) ), 403 ); } } elseif ( ! current_user_can( $capability ) ) { // Translators: the capability that the user is not authorized for. return new WP_Error( 'unauthorized', sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), $capability ), 403 ); } return true; } } jetpack/class.jetpack-json-api-themes-get-endpoint.php 0000644 00000000514 14722054026 0017021 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Themes get endpoint class. * * GET /sites/%s/themes/%s */ class Jetpack_JSON_API_Themes_Get_Endpoint extends Jetpack_JSON_API_Themes_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'switch_themes'; } jetpack/class.jetpack-json-api-plugins-delete-endpoint.php 0000644 00000006047 14722054026 0017707 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // POST /sites/%s/plugins/%s/delete new Jetpack_JSON_API_Plugins_Delete_Endpoint( array( 'description' => 'Delete/Uninstall a plugin from your jetpack blog', 'group' => '__do_not_document', 'stat' => 'plugins:1:delete', 'min_version' => '1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/delete', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(int|string) The plugin slug to delete', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/akismet%2Fakismet/delete', ) ); // v1.2 new Jetpack_JSON_API_Plugins_Delete_Endpoint( array( 'description' => 'Delete/Uninstall a plugin from your jetpack blog', 'group' => '__do_not_document', 'stat' => 'plugins:1:delete', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/delete', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(int|string) The plugin slug to delete', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/akismet%2Fakismet/delete', ) ); /** * Plugins delete endpoint class. * * POST /sites/%s/plugins/%s/delete */ class Jetpack_JSON_API_Plugins_Delete_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'delete_plugins'; /** * The action. * * @var string */ protected $action = 'delete'; /** * The delete function. * * @return bool|WP_Error */ protected function delete() { foreach ( $this->plugins as $plugin ) { if ( Jetpack::is_plugin_active( $plugin ) ) { $error = __( 'You cannot delete a plugin while it is active on the main site.', 'jetpack' ); $this->log[ $plugin ][] = $error; continue; } $result = delete_plugins( array( $plugin ) ); if ( is_wp_error( $result ) ) { $error = $result->get_error_message(); $this->log[ $plugin ][] = $error; } else { $this->log[ $plugin ][] = 'Plugin deleted'; } } if ( ! $this->bulk && isset( $error ) ) { return new WP_Error( 'delete_plugin_error', $error, 400 ); } return true; } } jetpack/class.jetpack-json-api-updates-status-endpoint.php 0000644 00000002276 14722054026 0017754 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Updates status class. * * GET /sites/%s/updates */ class Jetpack_JSON_API_Updates_Status extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'manage_options'; /** * Endpoint callback. * * @return array|WP_Error */ protected function result() { wp_update_themes(); wp_update_plugins(); $update_data = wp_get_update_data(); if ( ! isset( $update_data['counts'] ) ) { return new WP_Error( 'get_update_data_error', __( 'There was an error while getting the update data for this site.', 'jetpack' ), 500 ); } $result = $update_data['counts']; include ABSPATH . WPINC . '/version.php'; // $wp_version; $result['wp_version'] = isset( $wp_version ) ? $wp_version : null; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable if ( ! empty( $result['wordpress'] ) ) { $cur = get_preferred_from_update_core(); if ( isset( $cur->response ) && $cur->response === 'upgrade' ) { $result['wp_update_version'] = $cur->current; } } $result['jp_version'] = JETPACK__VERSION; return $result; } } jetpack/class.jetpack-json-api-translations-modify-endpoint.php 0000644 00000002271 14722054026 0020767 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Translations modify endpoint class. * POST /sites/%s/translation * POST /sites/%s/translations/update */ class Jetpack_JSON_API_Translations_Modify_Endpoint extends Jetpack_JSON_API_Translations_Endpoint { /** * The action. * * @var string */ protected $action = 'default_action'; /** * The new version. * * @var string */ protected $new_version; /** * The log. * * @var array */ protected $log; /** * Run the default action. * * @return true */ public function default_action() { $args = $this->input(); if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) { Jetpack_Options::update_option( 'autoupdate_translations', $args['autoupdate'] ); } return true; } /** * Update the translations. */ protected function update() { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; $upgrader = new Language_Pack_Upgrader( new Automatic_Upgrader_Skin() ); $result = $upgrader->bulk_upgrade(); $this->log = $upgrader->skin->get_upgrade_messages(); $this->success = ( ! is_wp_error( $result ) ) ? (bool) $result : false; } } jetpack/class.jetpack-json-api-get-option-backup-endpoint.php 0000644 00000003265 14722054026 0020315 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Get option backup endpoint. * * /sites/%s/options/backup -> $blog_id */ class Jetpack_JSON_API_Get_Option_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * Option names. * * @var string */ protected $option_names; /** * Validate input. * * @param object $object - unused. * * @return bool|WP_Error */ public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $query_args = $this->query_args(); if ( empty( $query_args['name'] ) ) { return new WP_Error( 'option_name_not_specified', __( 'You must specify an option name', 'jetpack' ), 400 ); } if ( is_array( $query_args['name'] ) ) { $this->option_names = $query_args['name']; } else { $this->option_names = array( $query_args['name'] ); } return true; } /** * The result. */ protected function result() { // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $options = array_map( array( $this, 'get_option_row' ), $this->option_names ); return array( 'options' => $options ); } /** * Get options row. * * @param string $name - name of the row. * * @return object|null Database query result or null on failure. */ private function get_option_row( $name ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) ); } } jetpack/class.jetpack-json-api-modules-modify-endpoint.php 0000644 00000004415 14722054026 0017720 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Modules modify endpoint class. * * POST /sites/%s/jetpack/modules/%s/activate * POST /sites/%s/jetpack/modules/%s * POST /sites/%s/jetpack/modules */ class Jetpack_JSON_API_Modules_Modify_Endpoint extends Jetpack_JSON_API_Modules_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'activate_plugins'; /** * The action. * * @var string */ protected $action = 'default_action'; /** * The default action. */ public function default_action() { $args = $this->input(); if ( isset( $args['active'] ) && is_bool( $args['active'] ) ) { if ( $args['active'] ) { return $this->activate_module(); } else { return $this->deactivate_module(); } } return true; } /** * Activate module. * * @return bool|WP_Error */ protected function activate_module() { foreach ( $this->modules as $module ) { if ( Jetpack::is_module_active( $module ) ) { $error = __( 'The Jetpack Module is already activated.', 'jetpack' ); $this->log[ $module ][] = $error; continue; } $result = Jetpack::activate_module( $module, false, false ); if ( false === $result || ! Jetpack::is_module_active( $module ) ) { $error = __( 'There was an error while activating the module.', 'jetpack' ); $this->log[ $module ][] = $error; } } if ( ! $this->bulk && isset( $error ) ) { return new WP_Error( 'activation_error', $error, 400 ); } return true; } /** * Deactivate module. * * @return bool|WP_Error */ protected function deactivate_module() { foreach ( $this->modules as $module ) { if ( ! Jetpack::is_module_active( $module ) ) { $error = __( 'The Jetpack Module is already deactivated.', 'jetpack' ); $this->log[ $module ] = $error; continue; } $result = Jetpack::deactivate_module( $module ); if ( false === $result || Jetpack::is_module_active( $module ) ) { $error = __( 'There was an error while deactivating the module.', 'jetpack' ); $this->log[ $module ] = $error; } } if ( ! $this->bulk && isset( $error ) ) { return new WP_Error( 'deactivation_error', $error, 400 ); } return true; } } jetpack/class.jetpack-json-api-plugins-modify-v1-2-endpoint.php 0000644 00000017216 14722054026 0020417 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint( array( 'description' => 'Activate/Deactivate a Plugin on your Jetpack Site, or set automatic updates', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s', 'stat' => 'plugins:1:modify', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(string) The plugin ID', ), 'request_format' => array( 'action' => '(string) Possible values are \'update\'', 'autoupdate' => '(bool) Whether or not to automatically update the plugin', 'active' => '(bool) Activate or deactivate the plugin', 'network_wide' => '(bool) Do action network wide (default value: false)', ), 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'action' => 'update', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/hello-dolly%20hello', ) ); new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint( array( 'description' => 'Activate/Deactivate a list of plugins on your Jetpack Site, or set automatic updates', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins', 'stat' => 'plugins:modify', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', ), 'request_format' => array( 'action' => '(string) Possible values are \'update\'', 'autoupdate' => '(bool) Whether or not to automatically update the plugin', 'active' => '(bool) Activate or deactivate the plugin', 'network_wide' => '(bool) Do action network wide (default value: false)', 'plugins' => '(array) A list of plugin ids to modify', ), 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => array( 'plugins' => '(array:plugin_v1_2) An array of plugin objects.', 'updated' => '(array) A list of plugin ids that were updated. Only present if action is update.', 'not_updated' => '(array) A list of plugin ids that were not updated. Only present if action is update.', 'log' => '(array) Update log. Only present if action is update.', ), 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'active' => true, 'plugins' => array( 'jetpack/jetpack', 'akismet/akismet', ), ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins', ) ); new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint( array( 'description' => 'Update a Plugin on your Jetpack Site', 'min_version' => '1.2', 'method' => 'POST', 'path' => '/sites/%s/plugins/%s/update/', 'stat' => 'plugins:1:update', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(string) The plugin ID', ), 'query_parameters' => array( 'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event', ), 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2, 'allow_jetpack_site_auth' => true, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/hello-dolly%20hello/update', ) ); /** * Plugins modify 1_2 Endpoint. */ class Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint extends Jetpack_JSON_API_Plugins_Modify_Endpoint { // phpcs:ignore PEAR.NamingConventions.ValidClassName.Invalid, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * Activate plugins. * * @return null|WP_Error null on success, WP_Error otherwise. */ protected function activate() { $permission_error = false; $has_errors = false; foreach ( $this->plugins as $plugin ) { // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check. if ( ! $this->accepts_site_based_authentication() ) { if ( ! $this->current_user_can( 'activate_plugin', $plugin ) ) { $this->log[ $plugin ]['error'] = __( 'Sorry, you are not allowed to activate this plugin.', 'jetpack' ); $has_errors = true; $permission_error = true; continue; } } if ( ( ! $this->network_wide && Jetpack::is_plugin_active( $plugin ) ) || is_plugin_active_for_network( $plugin ) ) { continue; } if ( ! $this->network_wide && is_network_only_plugin( $plugin ) && is_multisite() ) { $this->log[ $plugin ]['error'] = __( 'Plugin can only be Network Activated', 'jetpack' ); $has_errors = true; continue; } $result = activate_plugin( $plugin, '', $this->network_wide ); if ( is_wp_error( $result ) ) { $this->log[ $plugin ]['error'] = $result->get_error_messages(); $has_errors = true; continue; } $success = Jetpack::is_plugin_active( $plugin ); if ( $success && $this->network_wide ) { $success &= is_plugin_active_for_network( $plugin ); } if ( ! $success ) { $this->log[ $plugin ]['error'] = $result->get_error_messages; $has_errors = true; continue; } $this->log[ $plugin ][] = __( 'Plugin activated.', 'jetpack' ); } if ( ! $this->bulk && $has_errors ) { $plugin = $this->plugins[0]; if ( $permission_error ) { return new WP_Error( 'unauthorized_error', $this->log[ $plugin ]['error'], 403 ); } return new WP_Error( 'activation_error', $this->log[ $plugin ]['error'] ); } } /** * Deactivate plugins. * * @return null|WP_Error null on success, WP_Error otherwise. */ protected function deactivate() { $permission_error = false; foreach ( $this->plugins as $plugin ) { // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check. if ( ! $this->accepts_site_based_authentication() ) { if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) { $error = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' ); $this->log[ $plugin ]['error'] = $error; $permission_error = true; continue; } } if ( ! Jetpack::is_plugin_active( $plugin ) ) { continue; } deactivate_plugins( $plugin, false, $this->network_wide ); $success = ! Jetpack::is_plugin_active( $plugin ); if ( $success && $this->network_wide ) { $success &= ! is_plugin_active_for_network( $plugin ); } if ( ! $success ) { $error = __( 'There was an error deactivating your plugin', 'jetpack' ); $this->log[ $plugin ]['error'] = $error; continue; } $this->log[ $plugin ][] = __( 'Plugin deactivated.', 'jetpack' ); } if ( ! $this->bulk && isset( $error ) ) { if ( $permission_error ) { return new WP_Error( 'unauthorized_error', $error, 403 ); } return new WP_Error( 'deactivation_error', $error ); } } } jetpack/class.jetpack-json-api-translations-endpoint.php 0000644 00000001566 14722054026 0017510 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Translations endpoint class. * * GET /sites/%s/translations * POST /sites/%s/translations * POST /sites/%s/translations/update */ class Jetpack_JSON_API_Translations_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' ); /** * The log. * * @var array */ protected $log; /** * If we're successful. * * @var bool */ protected $success; /** * API Endpoint. * * @return array */ public function result() { return array( 'translations' => wp_get_translation_updates(), 'autoupdate' => Jetpack_Options::get_option( 'autoupdate_translations', false ), 'log' => $this->log, 'success' => $this->success, ); } } jetpack/class.jetpack-json-api-get-post-backup-endpoint.php 0000644 00000003034 14722054026 0017764 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Get post backup endpoint class. * * /sites/%s/posts/%d/backup -> $blog_id, $post_id */ class Jetpack_JSON_API_Get_Post_Backup_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var array */ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token /** * The post ID. * * @var int */ protected $post_id; /** * Validate the input. * * @param int $post_id - the post ID. */ public function validate_input( $post_id ) { if ( empty( $post_id ) || ! is_numeric( $post_id ) ) { return new WP_Error( 'post_id_not_specified', __( 'You must specify a Post ID', 'jetpack' ), 400 ); } $this->post_id = (int) $post_id; return true; } /** * The result. * * @return array|WP_Error */ protected function result() { global $wpdb; // Disable Sync as this is a read-only operation and triggered by sync activity. \Automattic\Jetpack\Sync\Actions::mark_sync_read_only(); $post = get_post( $this->post_id ); if ( empty( $post ) ) { return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack' ), 404 ); } // Fetch terms associated with this post object $terms = $wpdb->get_results( $wpdb->prepare( "SELECT term_taxonomy_id, term_order FROM {$wpdb->term_relationships} WHERE object_id = %d;", $post->ID ) ); return array( 'post' => (array) $post, 'meta' => get_post_meta( $post->ID ), 'terms' => (array) $terms, ); } } jetpack/class.jetpack-json-api-user-create-endpoint.php 0000644 00000005303 14722054026 0017177 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Connection\Utils; use Automattic\Jetpack\Constants; /** * User create endpoint class. */ class Jetpack_JSON_API_User_Create_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'create_users'; /** * User data. * * @var array */ private $user_data; /** * Endpoint callback. * * @return object|false */ public function result() { return $this->create_or_get_user(); } /** * Validate the input. * * @param object $object - the object. * * @return bool|WP_Error */ public function validate_input( $object ) { $this->user_data = $this->input(); if ( empty( $this->user_data ) ) { return new WP_Error( 'input_error', __( 'user_data is required', 'jetpack' ) ); } if ( ! isset( $this->user_data['email'] ) ) { return new WP_Error( 'input_error', __( 'user email is required', 'jetpack' ) ); } if ( ! isset( $this->user_data['login'] ) ) { return new WP_Error( 'input_error', __( 'user login is required', 'jetpack' ) ); } return parent::validate_input( $object ); } /** * Create or get the user. * * @return object|false */ public function create_or_get_user() { // Check for an existing user $user = get_user_by( 'email', $this->user_data['email'] ); $roles = (array) $this->user_data['roles']; $role = array_pop( $roles ); $query_args = $this->query_args(); if ( isset( $query_args['invite_accepted'] ) && $query_args['invite_accepted'] ) { Constants::set_constant( 'JETPACK_INVITE_ACCEPTED', true ); } if ( ! $user ) { // We modify the input here to mimick the same call structure of the update user endpoint. $this->user_data = (object) $this->user_data; $this->user_data->role = $role; $this->user_data->url = isset( $this->user_data->URL ) ? $this->user_data->URL : ''; $this->user_data->display_name = $this->user_data->name; $this->user_data->description = ''; $user = Utils::generate_user( $this->user_data ); } if ( is_multisite() ) { add_user_to_blog( get_current_blog_id(), $user->ID, $role ); } if ( ! $user ) { return false; } return $this->get_user( $user->ID ); } /** * Get the user. * * @param int $user_id - the user ID. * * @return object|WP_Error */ public function get_user( $user_id ) { $the_user = $this->get_author( $user_id, true ); if ( $the_user && ! is_wp_error( $the_user ) ) { $userdata = get_userdata( $user_id ); $the_user->roles = ! is_wp_error( $userdata ) ? $userdata->roles : array(); } return $the_user; } } jetpack/class.jetpack-json-api-core-modify-endpoint.php 0000644 00000005612 14722054026 0017200 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Core modify endpoint class. * * POST /sites/%s/core * POST /sites/%s/core/update */ class Jetpack_JSON_API_Core_Modify_Endpoint extends Jetpack_JSON_API_Core_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'update_core'; /** * Action. * * @var string */ protected $action = 'default_action'; /** * New version. * * @var string */ protected $new_version; /** * An array of log strings. * * @var array */ protected $log; /** * The default action. * * @return bool */ public function default_action() { $args = $this->input(); if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) { Jetpack_Options::update_option( 'autoupdate_core', $args['autoupdate'] ); } return true; } /** * Update the version. * * @return string|false|WP_Error New WordPress version on success, false or WP_Error on failure. */ protected function update() { $args = $this->input(); $version = isset( $args['version'] ) ? $args['version'] : false; $locale = isset( $args['locale'] ) ? $args['locale'] : get_locale(); include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; delete_site_transient( 'update_core' ); wp_version_check( array(), true ); if ( $version ) { $update = find_core_update( $version, $locale ); } else { $update = $this->find_latest_update_offer(); } /** * Pre-upgrade action * * @since 3.9.3 * * @param object|array $update as returned by find_core_update() or find_core_auto_update() */ do_action( 'jetpack_pre_core_upgrade', $update ); $skin = new Automatic_Upgrader_Skin(); $upgrader = new Core_Upgrader( $skin ); $this->new_version = $upgrader->upgrade( $update ); $this->log = $upgrader->skin->get_upgrade_messages(); if ( is_wp_error( $this->new_version ) ) { return $this->new_version; } return $this->new_version; } /** * Select the latest update. * Remove filters to bypass automattic updates. * * @return object|false The core update offering on success, false on failure. */ protected function find_latest_update_offer() { // Select the latest update. // Remove filters to bypass automattic updates. add_filter( 'request_filesystem_credentials', '__return_true' ); add_filter( 'automatic_updates_is_vcs_checkout', '__return_false' ); add_filter( 'allow_major_auto_core_updates', '__return_true' ); add_filter( 'send_core_update_notification_email', '__return_false' ); $update = find_core_auto_update(); remove_filter( 'request_filesystem_credentials', '__return_true' ); remove_filter( 'automatic_updates_is_vcs_checkout', '__return_false' ); remove_filter( 'allow_major_auto_core_updates', '__return_true' ); remove_filter( 'send_core_update_notification_email', '__return_false' ); return $update; } } jetpack/class.jetpack-json-api-plugins-get-endpoint.php 0000644 00000002365 14722054026 0017223 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * JSON API plugins get endpoint. */ new Jetpack_JSON_API_Plugins_Get_Endpoint( array( 'description' => 'Get the Plugin data.', 'method' => 'GET', 'path' => '/sites/%s/plugins/%s/', 'min_version' => '1', 'max_version' => '1.1', 'stat' => 'plugins:1', 'path_labels' => array( '$site' => '(int|string) The site ID, The site domain', '$plugin' => '(string) The plugin ID', ), 'allow_jetpack_site_auth' => true, 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format, 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello', ) ); /** * Plugins get endpoint class. * * GET /sites/%s/plugins/%s * * No v1.2 version since it is .com only */ class Jetpack_JSON_API_Plugins_Get_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'activate_plugins'; } jetpack/class.jetpack-json-api-themes-endpoint.php 0000644 00000014522 14722054026 0016250 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Image_CDN\Image_CDN_Core; /** * Base class for working with themes, has useful helper functions. */ abstract class Jetpack_JSON_API_Themes_Endpoint extends Jetpack_JSON_API_Endpoint { /** * The themes. * * @var array */ protected $themes = array(); /** * If we're working in bulk. * * @var boolean */ protected $bulk = true; /** * The log. * * @var array */ protected $log; /** * The current theme ID. * * @var int */ protected $current_theme_id; /** * The response format. * * @var array */ public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore 'id' => '(string) The theme\'s ID.', 'screenshot' => '(string) A theme screenshot URL', 'name' => '(string) The name of the theme.', 'theme_uri' => '(string) The URI of the theme\'s webpage.', 'description' => '(string) A description of the theme.', 'author' => '(string) The author of the theme.', 'author_uri' => '(string) The website of the theme author.', 'tags' => '(array) Tags indicating styles and features of the theme.', 'log' => '(array) An array of log strings', 'update' => '(array|null) An object containing information about the available update if there is an update available, null otherwise.', 'autoupdate' => '(bool) Whether the theme is automatically updated', 'autoupdate_translation' => '(bool) Whether the theme is automatically updating translations', ); /** * The result. */ protected function result() { $themes = $this->get_themes(); if ( ! $this->bulk && ! empty( $themes ) ) { return array_pop( $themes ); } return array( 'themes' => $themes ); } /** * Walks through either the submitted theme or list of themes and creates the global array * * @param string $theme - the theme URL. * * @return bool|WP_Error */ protected function validate_input( $theme ) { $args = $this->input(); // lets set what themes were requested, and validate them if ( ! isset( $theme ) || empty( $theme ) ) { if ( ! $args['themes'] || empty( $args['themes'] ) ) { return new WP_Error( 'missing_theme', __( 'You are required to specify a theme to update.', 'jetpack' ), 400 ); } if ( is_array( $args['themes'] ) ) { $this->themes = $args['themes']; } else { $this->themes[] = $args['themes']; } } else { $this->themes[] = urldecode( $theme ); $this->bulk = false; } $error = $this->validate_themes(); if ( is_wp_error( $error ) ) { return $error; } return parent::validate_input( $theme ); } /** * Walks through submitted themes to make sure they are valid * * @return bool|WP_Error */ protected function validate_themes() { foreach ( $this->themes as $theme ) { $error = wp_get_theme( $theme )->errors(); if ( is_wp_error( $error ) ) { return new WP_Error( 'unknown_theme', $error->get_error_messages(), 404 ); } } return true; } /** * Format a theme for the public API * * @param object $theme WP_Theme object. * @return array Named array of theme info used by the API */ protected function format_theme( $theme ) { if ( ! ( $theme instanceof WP_Theme ) ) { $theme = wp_get_theme( $theme ); } $fields = array( 'name' => 'Name', 'theme_uri' => 'ThemeURI', 'description' => 'Description', 'author' => 'Author', 'author_uri' => 'AuthorURI', 'tags' => 'Tags', 'version' => 'Version', ); $id = $theme->get_stylesheet(); $formatted_theme = array( 'id' => $id, 'screenshot' => Image_CDN_Core::cdn_url( $theme->get_screenshot(), array(), 'network_path' ), 'active' => $id === $this->current_theme_id, ); foreach ( $fields as $key => $field ) { $formatted_theme[ $key ] = $theme->get( $field ); } $update_themes = get_site_transient( 'update_themes' ); $formatted_theme['update'] = ( isset( $update_themes->response[ $id ] ) ) ? $update_themes->response[ $id ] : null; $autoupdate = in_array( $id, Jetpack_Options::get_option( 'autoupdate_themes', array() ), true ); $formatted_theme['autoupdate'] = $autoupdate; $autoupdate_translation = in_array( $id, Jetpack_Options::get_option( 'autoupdate_themes_translations', array() ), true ); $formatted_theme['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false ); if ( isset( $this->log[ $id ] ) ) { $formatted_theme['log'] = $this->log[ $id ]; } /** * Filter the array of theme information that will be returned per theme by the Jetpack theme APIs. * * @module json-api * * @since 4.7.0 * * @param array $formatted_theme The theme info array. */ return apply_filters( 'jetpack_format_theme_details', $formatted_theme ); } /** * Checks the query_args our collection endpoint was passed to ensure that it's in the proper bounds. * * @return bool|WP_Error a WP_Error object if the args are out of bounds, true if things are good. */ protected function check_query_args() { $args = $this->query_args(); if ( $args['offset'] < 0 ) { return new WP_Error( 'invalid_offset', __( 'Offset must be greater than or equal to 0.', 'jetpack' ), 400 ); } if ( $args['limit'] < 0 ) { return new WP_Error( 'invalid_limit', __( 'Limit must be greater than or equal to 0.', 'jetpack' ), 400 ); } return true; } /** * Format a list of themes for public display, using the supplied offset and limit args * * @uses WPCOM_JSON_API_Endpoint::query_args() * @return array Public API theme objects */ protected function get_themes() { // ditch keys $themes = array_values( $this->themes ); // do offset & limit - we've already returned a 400 error if they're bad numbers $args = $this->query_args(); if ( isset( $args['offset'] ) ) { $themes = array_slice( $themes, (int) $args['offset'] ); } if ( isset( $args['limit'] ) ) { $themes = array_slice( $themes, 0, (int) $args['limit'] ); } $this->current_theme_id = wp_get_theme()->get_stylesheet(); return array_map( array( $this, 'format_theme' ), $themes ); } } jetpack/class.jetpack-json-api-plugins-endpoint.php 0000644 00000040574 14722054026 0016452 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Constants; use Automattic\Jetpack\Current_Plan; use Automattic\Jetpack\Sync\Functions; /** * Base class for working with plugins. */ abstract class Jetpack_JSON_API_Plugins_Endpoint extends Jetpack_JSON_API_Endpoint { /** * Plugins. * * @var array */ protected $plugins = array(); /** * If the plugin is network wide. * * @var boolean */ protected $network_wide = false; /** * If we're working in bulk. * * @var boolean */ protected $bulk = true; /** * The log. * * @var array */ protected $log; /** * If the request is a scheduled update. * * @var boolean */ protected $scheduled_update = false; /** * Response format. * * @var array */ public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore 'id' => '(safehtml) The plugin\'s ID', 'slug' => '(safehtml) The plugin\'s .org slug', 'active' => '(boolean) The plugin status.', 'update' => '(object) The plugin update info.', 'name' => '(safehtml) The name of the plugin.', 'plugin_url' => '(url) Link to the plugin\'s web site.', 'version' => '(safehtml) The plugin version number.', 'description' => '(safehtml) Description of what the plugin does and/or notes from the author', 'author' => '(safehtml) The author\'s name', 'author_url' => '(url) The authors web site address', 'network' => '(boolean) Whether the plugin can only be activated network wide.', 'autoupdate' => '(boolean) Whether the plugin is automatically updated', 'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations', 'next_autoupdate' => '(string) Y-m-d H:i:s for next scheduled update event', 'log' => '(array:safehtml) An array of update log strings.', 'uninstallable' => '(boolean) Whether the plugin is unistallable.', 'action_links' => '(array) An array of action links that the plugin uses.', ); /** * Response format v1_2 * * @var array */ public static $_response_format_v1_2 = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore 'slug' => '(safehtml) The plugin\'s .org slug', 'active' => '(boolean) The plugin status.', 'update' => '(object) The plugin update info.', 'name' => '(safehtml) The plugin\'s ID', 'display_name' => '(safehtml) The name of the plugin.', 'version' => '(safehtml) The plugin version number.', 'description' => '(safehtml) Description of what the plugin does and/or notes from the author', 'author' => '(safehtml) The author\'s name', 'author_url' => '(url) The authors web site address', 'plugin_url' => '(url) Link to the plugin\'s web site.', 'network' => '(boolean) Whether the plugin can only be activated network wide.', 'autoupdate' => '(boolean) Whether the plugin is automatically updated', 'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations', 'uninstallable' => '(boolean) Whether the plugin is unistallable.', 'action_links' => '(array) An array of action links that the plugin uses.', 'log' => '(array:safehtml) An array of update log strings.', ); /** * The result. * * @return array */ protected function result() { $plugins = $this->get_plugins(); if ( ! $this->bulk && ! empty( $plugins ) ) { return array_pop( $plugins ); } return array( 'plugins' => $plugins ); } /** * Validate the input. * * @param string $plugin - the plugin we're validating. * * @return bool|WP_Error */ protected function validate_input( $plugin ) { $error = parent::validate_input( $plugin ); if ( is_wp_error( $error ) ) { return $error; } $error = $this->validate_network_wide(); if ( is_wp_error( $error ) ) { return $error; } $error = $this->validate_scheduled_update(); if ( is_wp_error( $error ) ) { return $error; } $args = $this->input(); // find out what plugin, or plugins we are dealing with // validate the requested plugins if ( ! isset( $plugin ) || empty( $plugin ) ) { if ( ! $args['plugins'] || empty( $args['plugins'] ) ) { return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin.', 'jetpack' ), 400 ); } if ( is_array( $args['plugins'] ) ) { $this->plugins = $args['plugins']; } else { $this->plugins[] = $args['plugins']; } } else { $this->bulk = false; $this->plugins[] = urldecode( $plugin ); } $error = $this->validate_plugins(); if ( is_wp_error( $error ) ) { return $error; } return true; } /** * Walks through submitted plugins to make sure they are valid * * @return bool|WP_Error */ protected function validate_plugins() { if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) { return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) ); } foreach ( $this->plugins as $index => $plugin ) { if ( ! preg_match( '/\.php$/', $plugin ) ) { $plugin = $plugin . '.php'; $this->plugins[ $index ] = $plugin; } $valid = $this->validate_plugin( urldecode( $plugin ) ); if ( is_wp_error( $valid ) ) { return $valid; } } return true; } /** * Format the plugin. * * @param string $plugin_file - the plugin file. * @param array $plugin_data - the plugin data. * * @return array */ protected function format_plugin( $plugin_file, $plugin_data ) { if ( version_compare( $this->min_version, '1.2', '>=' ) ) { return $this->format_plugin_v1_2( $plugin_file, $plugin_data ); } $plugin = array(); $plugin['id'] = preg_replace( '/(.+)\.php$/', '$1', $plugin_file ); $plugin['slug'] = Jetpack_Autoupdate::get_plugin_slug( $plugin_file ); $plugin['active'] = Jetpack::is_plugin_active( $plugin_file ); $plugin['name'] = $plugin_data['Name']; $plugin['plugin_url'] = $plugin_data['PluginURI']; $plugin['version'] = $plugin_data['Version']; $plugin['description'] = $plugin_data['Description']; $plugin['author'] = $plugin_data['Author']; $plugin['author_url'] = $plugin_data['AuthorURI']; $plugin['network'] = $plugin_data['Network']; $plugin['update'] = $this->get_plugin_updates( $plugin_file ); $plugin['next_autoupdate'] = gmdate( 'Y-m-d H:i:s', wp_next_scheduled( 'wp_maybe_auto_update' ) ); $action_link = $this->get_plugin_action_links( $plugin_file ); if ( ! empty( $action_link ) ) { $plugin['action_links'] = $action_link; } $plugin['plugin'] = $plugin_file; if ( ! class_exists( 'WP_Automatic_Updater' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } $autoupdate = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR ); $plugin['autoupdate'] = $autoupdate; $autoupdate_translation = in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true ); $plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false ); $plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file ); if ( is_multisite() ) { $plugin['network_active'] = is_plugin_active_for_network( $plugin_file ); } if ( ! empty( $this->log[ $plugin_file ] ) ) { $plugin['log'] = $this->log[ $plugin_file ]; } return $plugin; } /** * Format the plugin for v1_2. * * @param string $plugin_file - the plugin file. * @param array $plugin_data - the plugin data. * * @return array */ protected function format_plugin_v1_2( $plugin_file, $plugin_data ) { $plugin = array(); $plugin['slug'] = Jetpack_Autoupdate::get_plugin_slug( $plugin_file ); $plugin['active'] = Jetpack::is_plugin_active( $plugin_file ); $plugin['name'] = preg_replace( '/(.+)\.php$/', '$1', $plugin_file ); $plugin['display_name'] = $plugin_data['Name']; $plugin['plugin_url'] = $plugin_data['PluginURI']; $plugin['version'] = $plugin_data['Version']; $plugin['description'] = $plugin_data['Description']; $plugin['author'] = $plugin_data['Author']; $plugin['author_url'] = $plugin_data['AuthorURI']; $plugin['network'] = $plugin_data['Network']; $plugin['update'] = $this->get_plugin_updates( $plugin_file ); $action_link = $this->get_plugin_action_links( $plugin_file ); if ( ! empty( $action_link ) ) { $plugin['action_links'] = $action_link; } $plugin['plugin'] = $plugin_file; if ( ! class_exists( 'WP_Automatic_Updater' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } $autoupdate = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR ); $plugin['autoupdate'] = $autoupdate; $autoupdate_translation = $this->plugin_has_translations_autoupdates_enabled( $plugin_file ); $plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false ); $plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file ); if ( is_multisite() ) { $plugin['network_active'] = is_plugin_active_for_network( $plugin_file ); } if ( ! empty( $this->log[ $plugin_file ] ) ) { $plugin['log'] = $this->log[ $plugin_file ]; } return $plugin; } /** * Check if plugin has autoupdates for translations enabled. * * @param string $plugin_file - the plugin file. * * @return bool */ protected function plugin_has_translations_autoupdates_enabled( $plugin_file ) { return (bool) in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true ); } /** * Get file mod capabilities. */ protected function get_file_mod_capabilities() { $reasons_can_not_autoupdate = array(); $reasons_can_not_modify_files = array(); $has_file_system_write_access = Functions::file_system_write_access(); if ( ! $has_file_system_write_access ) { $reasons_can_not_modify_files['has_no_file_system_write_access'] = __( 'The file permissions on this host prevent editing files.', 'jetpack' ); } $disallow_file_mods = Constants::get_constant( 'DISALLOW_FILE_MODS' ); if ( $disallow_file_mods ) { $reasons_can_not_modify_files['disallow_file_mods'] = __( 'File modifications are explicitly disabled by a site administrator.', 'jetpack' ); } $automatic_updater_disabled = Constants::get_constant( 'AUTOMATIC_UPDATER_DISABLED' ); if ( $automatic_updater_disabled ) { $reasons_can_not_autoupdate['automatic_updater_disabled'] = __( 'Any autoupdates are explicitly disabled by a site administrator.', 'jetpack' ); } if ( is_multisite() ) { // is it the main network ? is really is multi network if ( Jetpack::is_multi_network() ) { $reasons_can_not_modify_files['is_multi_network'] = __( 'Multi network install are not supported.', 'jetpack' ); } // Is the site the main site here. if ( ! is_main_site() ) { $reasons_can_not_modify_files['is_sub_site'] = __( 'The site is not the main network site', 'jetpack' ); } } $file_mod_capabilities = array( 'modify_files' => (bool) empty( $reasons_can_not_modify_files ), // install, remove, update 'autoupdate_files' => (bool) empty( $reasons_can_not_modify_files ) && empty( $reasons_can_not_autoupdate ), // enable autoupdates ); if ( ! empty( $reasons_can_not_modify_files ) ) { $file_mod_capabilities['reasons_modify_files_unavailable'] = $reasons_can_not_modify_files; } if ( ! $file_mod_capabilities['autoupdate_files'] ) { $file_mod_capabilities['reasons_autoupdate_unavailable'] = array_merge( $reasons_can_not_autoupdate, $reasons_can_not_modify_files ); } return $file_mod_capabilities; } /** * Get plugins. * * @return array */ protected function get_plugins() { $plugins = array(); /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ $installed_plugins = apply_filters( 'all_plugins', get_plugins() ); foreach ( $this->plugins as $plugin ) { if ( ! isset( $installed_plugins[ $plugin ] ) ) { continue; } $formatted_plugin = $this->format_plugin( $plugin, $installed_plugins[ $plugin ] ); // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check. if ( $this->accepts_site_based_authentication() ) { $plugins[] = $formatted_plugin; continue; } /* * Do not show network-active plugins * to folks who do not have the permission to see them. */ if ( /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */ ! apply_filters( 'show_network_active_plugins', current_user_can( 'manage_network_plugins' ) ) && ! empty( $formatted_plugin['network_active'] ) && true === $formatted_plugin['network_active'] ) { continue; } $plugins[] = $formatted_plugin; } $args = $this->query_args(); if ( isset( $args['offset'] ) ) { $plugins = array_slice( $plugins, (int) $args['offset'] ); } if ( isset( $args['limit'] ) ) { $plugins = array_slice( $plugins, 0, (int) $args['limit'] ); } return $plugins; } /** * Validate network wide. * * @return bool|WP_Error */ protected function validate_network_wide() { $args = $this->input(); if ( isset( $args['network_wide'] ) && $args['network_wide'] ) { $this->network_wide = true; } // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check. if ( $this->accepts_site_based_authentication() ) { return true; } if ( $this->network_wide && ! current_user_can( 'manage_network_plugins' ) ) { return new WP_Error( 'unauthorized', __( 'This user is not authorized to manage plugins network wide.', 'jetpack' ), 403 ); } return true; } /** * Validate the plugin. * * @param string $plugin - the plugin we're validating. * * @return bool|WP_Error */ protected function validate_plugin( $plugin ) { if ( ! isset( $plugin ) || empty( $plugin ) ) { return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin to activate.', 'jetpack' ), 400 ); } $error = validate_plugin( $plugin ); if ( is_wp_error( $error ) ) { return new WP_Error( 'unknown_plugin', $error->get_error_messages(), 404 ); } return true; } /** * Validates if scheduled updates are allowed based on the current plan. * * @return bool|WP_Error True if scheduled updates are allowed or not provided, WP_Error otherwise. */ protected function validate_scheduled_update() { $args = $this->input(); if ( isset( $args['scheduled_update'] ) && $args['scheduled_update'] ) { if ( Current_Plan::supports( 'scheduled-updates' ) ) { $this->scheduled_update = true; } else { return new WP_Error( 'unauthorized', __( 'Scheduled updates are not available on your current plan. Please upgrade to a plan that supports scheduled updates to use this feature.', 'jetpack' ), 403 ); } } return true; } /** * Get plugin updates. * * @param string $plugin_file - the plugin file. * * @return object|null */ protected function get_plugin_updates( $plugin_file ) { $plugin_updates = get_plugin_updates(); if ( isset( $plugin_updates[ $plugin_file ] ) ) { $update = $plugin_updates[ $plugin_file ]->update; $cleaned_update = array(); foreach ( (array) $update as $update_key => $update_value ) { switch ( $update_key ) { case 'id': case 'slug': case 'plugin': case 'new_version': case 'tested': $cleaned_update[ $update_key ] = wp_kses( $update_value, array() ); break; case 'url': case 'package': $cleaned_update[ $update_key ] = esc_url( $update_value ); break; } } return (object) $cleaned_update; } return null; } /** * Get plugin action links. * * @param string $plugin_file - the plugin file. * * @return array */ protected function get_plugin_action_links( $plugin_file ) { return Functions::get_plugins_action_links( $plugin_file ); } } jetpack/class.jetpack-json-api-modules-get-endpoint.php 0000644 00000000532 14722054026 0017204 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The Modules get endpoint. * * /sites/%s/jetpack/modules/%s */ class Jetpack_JSON_API_Modules_Get_Endpoint extends Jetpack_JSON_API_Modules_Endpoint { /** * Needed capabilities. * * @var string */ protected $needed_capabilities = 'jetpack_manage_modules'; } jetpack/class-jetpack-json-api-delete-backup-helper-script-endpoint.php 0000644 00000004206 14722054026 0022244 0 ustar 00 <?php /** * API endpoint /sites/%s/delete-backup-helper-script * This API endpoint deletes a Jetpack Backup Helper Script * * @package automattic/jetpack */ use Automattic\Jetpack\Backup\V0004\Helper_Script_Manager; /** * API endpoint /sites/%s/delete-backup-helper-script * This API endpoint deletes a Jetpack Backup Helper Script */ class Jetpack_JSON_API_Delete_Backup_Helper_Script_Endpoint extends Jetpack_JSON_API_Endpoint { /** * This endpoint is only accessible from Jetpack Backup; it requires no further capabilities. * * @var array */ protected $needed_capabilities = array(); /** * Method to call when running this endpoint (delete) * * @var string */ protected $action = 'delete'; /** * Local path to the Helper Script to delete. * * @var string|null */ protected $script_path = null; /** * An array with 'success' => true if the specified file has been successfully deleted, or an instance of WP_Error. * * @var array|WP_Error */ protected $result; /** * Checks that the input args look like a valid Helper Script path. * * @param null $object Unused. * @return bool|WP_Error a WP_Error object or true if the input seems ok. */ protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $args = $this->input(); if ( ! isset( $args['path'] ) ) { return new WP_Error( 'invalid_args', __( 'You must specify a helper script path', 'jetpack' ), 400 ); } $this->script_path = $args['path']; return true; } /** * Deletes the specified Helper Script. */ protected function delete() { $delete_result = Helper_Script_Manager::delete_helper_script( $this->script_path ); Helper_Script_Manager::cleanup_expired_helper_scripts(); if ( is_wp_error( $delete_result ) ) { $this->result = $delete_result; } else { $this->result = array( 'success' => true ); } } /** * Returns the success or failure of the deletion operation * * @return array An array containing one key; 'success', which specifies whether the operation was successful. */ protected function result() { return $this->result; } } jetpack/class.wpcom-json-api-update-option-endpoint.php 0000644 00000002435 14722054026 0017257 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update option endpoint. */ class WPCOM_JSON_API_Update_Option_Endpoint extends WPCOM_JSON_API_Get_Option_Endpoint { /** * The option value. * * @var string */ public $option_value; /** * Endpoint callback. * * @return array */ public function result() { if ( $this->site_option ) { update_site_option( $this->option_name, $this->option_value ); } else { update_option( $this->option_name, $this->option_value ); } return parent::result(); } /** * Validate the input. * * @param object $object - the object we're validating. * * @return bool|WP_Error */ public function validate_input( $object ) { $input = $this->input(); $query_args = $this->query_args(); if ( ! isset( $input['option_value'] ) || is_array( $input['option_value'] ) ) { return new WP_Error( 'option_value_not_set', __( 'You must specify an option_value', 'jetpack' ) ); } if ( $query_args['is_array'] ) { // When converted back from JSON, the value is an object. // Cast it to an array for options that expect arrays. $this->option_value = (array) $input['option_value']; } else { $this->option_value = $input['option_value']; } return parent::validate_input( $object ); } } class.wpcom-json-api-get-media-v1-2-endpoint.php 0000644 00000010062 14722054026 0015360 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php'; new WPCOM_JSON_API_Get_Media_v1_2_Endpoint( array( 'description' => 'Get a single media item (by ID).', 'group' => 'media', 'stat' => 'media:1', 'min_version' => '1.2', 'max_version' => '1.2', 'method' => 'GET', 'path' => '/sites/%s/media/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'response_format' => array( 'ID' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'post_ID' => '(int) ID of the post this media is attached to', 'author_ID' => '(int) ID of the user who uploaded the media', 'URL' => '(string) URL to the file', 'guid' => '(string) Unique identifier', 'file' => '(string) Filename', 'extension' => '(string) File extension', 'mime_type' => '(string) File MIME type', 'title' => '(string) Filename', 'caption' => '(string) User-provided caption of the file', 'description' => '(string) Description of the file', 'alt' => '(string) Alternative text for image files.', 'thumbnails' => '(object) Media item thumbnail URL options', 'height' => '(int) (Image & video only) Height of the media item', 'width' => '(int) (Image & video only) Width of the media item', 'length' => '(int) (Video & audio only) Duration of the media item, in seconds', 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item', 'rating' => '(string) (Video only) VideoPress rating of the video', 'display_embed' => '(string) Video only. Whether to share or not the video.', 'allow_download' => '(string) Video only. Whether the video can be downloaded or not.', 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress', 'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.', 'revision_history' => '(object) An object with `items` and `original` keys. ' . '`original` is an object with data about the original image. ' . '`items` is an array of snapshots of the previous images of this Media. ' . 'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/934', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * GET Media v1_2 endpoint class. */ class WPCOM_JSON_API_Get_Media_v1_2_Endpoint extends WPCOM_JSON_API_Get_Media_v1_1_Endpoint { //phpcs:ignore /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $response = parent::callback( $path, $blog_id, $media_id ); if ( is_wp_error( $response ) ) { return $response; } $media_item = get_post( $media_id ); $response->modified = (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified ); // expose `revision_history` object. $response->revision_history = (object) array( 'items' => (array) Jetpack_Media::get_revision_history( $media_id ), 'original' => (object) Jetpack_Media::get_original_media( $media_id ), ); return $response; } } class.wpcom-json-api-list-shortcodes-endpoint.php 0000644 00000003415 14722054026 0016173 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List shortcodes endpoint. */ new WPCOM_JSON_API_List_Shortcodes_Endpoint( array( 'description' => 'Get a list of shortcodes available on a site. Note: The current user must have publishing access.', 'group' => 'sites', 'stat' => 'shortcodes', 'method' => 'GET', 'path' => '/sites/%s/shortcodes', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'response_format' => array( 'shortcodes' => '(array) A list of supported shortcodes by their handle.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcodes', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List shortcodes endpoint class * * /sites/%s/shortcodes -> $blog_id */ class WPCOM_JSON_API_List_Shortcodes_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // permissions check. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 ); } // list em. global $shortcode_tags; $output = array( 'shortcodes' => array() ); foreach ( $shortcode_tags as $tag => $class ) { if ( '__return_false' === $class ) { continue; } $output['shortcodes'][] = $tag; } return $output; } } class.wpcom-json-api-get-media-endpoint.php 0000644 00000004143 14722054026 0014700 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Get_Media_Endpoint( array( 'description' => 'Get a single media item (by ID).', 'group' => 'media', 'stat' => 'media:1', 'method' => 'GET', 'path' => '/sites/%s/media/%d', 'deprecated' => true, 'new_version' => '1.1', 'max_version' => '1', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'response_format' => array( 'id' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'parent' => '(int) ID of the post this media is attached to', 'link' => '(string) URL to the file', 'title' => '(string) Filename', 'caption' => '(string) User-provided caption of the file', 'description' => '(string) Description of the file', 'metadata' => '(array) Array of metadata about the file, such as Exif data or sizes', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/934', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * GET Media endpoint class. */ class WPCOM_JSON_API_Get_Media_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $media_id - the media ID. */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // upload_files can probably be used for other endpoints but we want contributors to be able to use media too. if ( ! current_user_can( 'edit_posts', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } return $this->get_media_item( $media_id ); } } class.wpcom-json-api-get-comment-endpoint.php 0000644 00000003240 14722054026 0015260 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoint: /sites/%s/comments/%d -> $blog_id, $comment_id */ new WPCOM_JSON_API_Get_Comment_Endpoint( array( 'description' => 'Get a single comment.', 'group' => 'comments', 'stat' => 'comments:1', 'method' => 'GET', 'path' => '/sites/%s/comments/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$comment_ID' => '(int) The comment ID', ), 'allow_fallback_to_jetpack_blog_token' => true, 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/147564', ) ); /** * Get Comment endpoint class. */ class WPCOM_JSON_API_Get_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $comment_id - the comment ID. */ public function callback( $path = '', $blog_id = 0, $comment_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $return = $this->get_comment( $comment_id, $args['context'] ); if ( ! $return || is_wp_error( $return ) ) { return $return; } /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'comments' ); return $return; } } class.wpcom-json-api-comment-endpoint.php 0000644 00000021471 14722054026 0014511 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Comment endpoint. * * @todo - can this file be written without overriding global variables? * * @phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited */ /** * Comment endpoint class. */ abstract class WPCOM_JSON_API_Comment_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Comment object array. * * @var $comment_object_format */ public $comment_object_format = array( // explicitly document and cast all output. 'ID' => '(int) The comment ID.', 'post' => "(object>post_reference) A reference to the comment's post.", 'author' => '(object>author) The author of the comment.', 'date' => "(ISO 8601 datetime) The comment's creation time.", 'URL' => '(URL) The full permalink URL to the comment.', 'short_URL' => '(URL) The wp.me short URL.', 'content' => '(HTML) <code>context</code> dependent.', 'raw_content' => '(string) Raw comment content.', 'status' => array( 'approved' => 'The comment has been approved.', 'unapproved' => 'The comment has been held for review in the moderation queue.', 'spam' => 'The comment has been marked as spam.', 'trash' => 'The comment is in the trash.', ), 'parent' => "(object>comment_reference|false) A reference to the comment's parent, if it has one.", 'type' => array( 'comment' => 'The comment is a regular comment.', 'trackback' => 'The comment is a trackback.', 'pingback' => 'The comment is a pingback.', 'review' => 'The comment is a product review.', ), 'like_count' => '(int) The number of likes for this comment.', 'i_like' => '(bool) Does the current user like this comment?', 'meta' => '(object) Meta data', 'can_moderate' => '(bool) Whether current user can moderate the comment.', 'i_replied' => '(bool) Has the current user replied to this comment?', ); /** * Class constructor. * * @param object $args - arguments passed to constructor. */ public function __construct( $args ) { if ( ! $this->response_format ) { $this->response_format =& $this->comment_object_format; } parent::__construct( $args ); } /** * Get the comment. * * @param int $comment_id - the ID of the comment. * @param string $context - the context of the comment (displayed or edited). */ public function get_comment( $comment_id, $context ) { global $blog_id; $comment = get_comment( $comment_id ); if ( ! $comment || is_wp_error( $comment ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } $types = array( '', 'comment', 'pingback', 'trackback', 'review' ); // @todo - can we make this comparison strict without breaking anything? // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict if ( ! in_array( $comment->comment_type, $types ) ) { return new WP_Error( 'unknown_comment', 'Unknown comment', 404 ); } $post = get_post( $comment->comment_post_ID ); if ( ! $post || is_wp_error( $post ) ) { return new WP_Error( 'unknown_post', 'Unknown post', 404 ); } $status = wp_get_comment_status( $comment->comment_ID ); // Permissions. switch ( $context ) { case 'edit': if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 ); } $GLOBALS['post'] = $post; $comment = get_comment_to_edit( $comment->comment_ID ); foreach ( array( 'comment_author', 'comment_author_email', 'comment_author_url' ) as $field ) { $comment->$field = htmlspecialchars_decode( $comment->$field, ENT_QUOTES ); } break; case 'display': if ( 'approved' !== $status ) { $current_user_id = get_current_user_id(); $user_can_read_comment = false; // @todo - can we make this comparison strict without breaking anything? // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual if ( $current_user_id && $comment->user_id && $current_user_id == $comment->user_id ) { $user_can_read_comment = true; } elseif ( $comment->comment_author_email && $comment->comment_author && isset( $this->api->token_details['user'] ) && isset( $this->api->token_details['user']['user_email'] ) && $this->api->token_details['user']['user_email'] === $comment->comment_author_email && $this->api->token_details['user']['display_name'] === $comment->comment_author ) { $user_can_read_comment = true; } else { $user_can_read_comment = current_user_can( 'edit_posts' ); } if ( ! $user_can_read_comment ) { return new WP_Error( 'unauthorized', 'User cannot read unapproved comment', 403 ); } } $GLOBALS['post'] = $post; setup_postdata( $post ); break; default: return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 ); } $can_view = $this->user_can_view_post( $post->ID ); if ( ! $can_view || is_wp_error( $can_view ) ) { return $can_view; } $GLOBALS['comment'] = $comment; $response = array(); foreach ( array_keys( $this->comment_object_format ) as $key ) { switch ( $key ) { case 'ID': // explicitly cast all output. $response[ $key ] = (int) $comment->comment_ID; break; case 'post': $response[ $key ] = (object) array( 'ID' => (int) $post->ID, 'title' => (string) get_the_title( $post->ID ), 'type' => (string) $post->post_type, 'link' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ), ); break; case 'author': $response[ $key ] = (object) $this->get_author( $comment, current_user_can( 'edit_comment', $comment->comment_ID ) ); break; case 'date': $response[ $key ] = (string) $this->format_date( $comment->comment_date_gmt, $comment->comment_date ); break; case 'URL': $response[ $key ] = (string) esc_url_raw( get_comment_link( $comment->comment_ID ) ); break; case 'short_URL': // @todo - pagination $response[ $key ] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) . "%23comment-{$comment->comment_ID}" ); break; case 'content': if ( 'display' === $context ) { ob_start(); comment_text(); $response[ $key ] = (string) ob_get_clean(); } else { $response[ $key ] = (string) $comment->comment_content; } break; case 'raw_content': $response[ $key ] = (string) $comment->comment_content; break; case 'status': $response[ $key ] = (string) $status; break; case 'parent': // May be object or false. $parent = $comment->comment_parent ? get_comment( $comment->comment_parent ) : null; if ( $parent ) { $response[ $key ] = (object) array( 'ID' => (int) $parent->comment_ID, 'type' => (string) ( $parent->comment_type ? $parent->comment_type : 'comment' ), 'link' => (string) $this->links->get_comment_link( $blog_id, $parent->comment_ID ), ); } else { $response[ $key ] = false; } break; case 'type': $response[ $key ] = (string) ( $comment->comment_type ? $comment->comment_type : 'comment' ); break; case 'like_count': if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $response[ $key ] = (int) $this->api->comment_like_count( $blog_id, $post->ID, $comment->comment_ID ); } break; case 'i_like': if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $response[ $key ] = (bool) Likes::comment_like_current_user_likes( $blog_id, (int) $comment->comment_ID ); } break; case 'meta': $response[ $key ] = (object) array( 'links' => (object) array( 'self' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID ), 'help' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'help' ), 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ), 'post' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $comment->comment_post_ID ), 'replies' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'replies/' ), 'likes' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'likes/' ), ), ); break; case 'can_moderate': $response[ $key ] = (bool) current_user_can( 'edit_comment', $comment_id ); break; case 'i_replied': $response[ $key ] = (bool) 0 < get_comments( array( 'user_id' => get_current_user_id(), 'parent' => $comment->comment_ID, 'count' => true, ) ); break; } } unset( $GLOBALS['comment'], $GLOBALS['post'] ); return $response; } } class.wpcom-json-api-bulk-update-comments-endpoint.php 0000644 00000016562 14722054026 0017114 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoints: /sites/%s/comments/status * /sites/%s/comments/delete */ new WPCOM_JSON_API_Bulk_Update_Comments_Endpoint( array( 'description' => 'Update multiple comment\'s status.', 'group' => 'comments', 'stat' => 'comments:1:bulk-update-status', 'min_version' => '1', 'max_version' => '1', 'method' => 'POST', 'path' => '/sites/%s/comments/status', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'comment_ids' => '(array|string) An array, or comma-separated list, of Comment IDs to update.', 'status' => '(string) The new status value. Allowed values: approved, unapproved, spam, trash', ), 'response_format' => array( 'results' => '(array) An array of updated Comment IDs.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/status', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'comment_ids' => array( 881, 882 ), 'status' => 'approved', ), ), ) ); new WPCOM_JSON_API_Bulk_Update_Comments_Endpoint( array( 'description' => 'Permanently delete multiple comments. Note: this request will send non-trashed comments to the trash. Trashed comments will be permanently deleted.', 'group' => 'comments', 'stat' => 'comments:1:bulk-delete', 'min_version' => '1', 'max_version' => '1', 'method' => 'POST', 'path' => '/sites/%s/comments/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'comment_ids' => '(array|string) An array, or comma-separated list, of Comment IDs to delete or trash. (optional)', 'empty_status' => '(string) Force to permanently delete all spam or trash comments. (optional). Allowed values: spam, trash', ), 'response_format' => array( 'results' => '(array) An array of deleted or trashed Comment IDs.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'comment_ids' => array( 881, 882 ), ), ), ) ); /** * Bulk update comments endpoint class. */ class WPCOM_JSON_API_Bulk_Update_Comments_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $input = $this->input(); if ( isset( $input['comment_ids'] ) && is_array( $input['comment_ids'] ) ) { $comment_ids = $input['comment_ids']; } elseif ( isset( $input['comment_ids'] ) && ! empty( $input['comment_ids'] ) ) { $comment_ids = explode( ',', $input['comment_ids'] ); } else { $comment_ids = array(); } $result = array( 'results' => array(), ); wp_defer_comment_counting( true ); if ( $this->api->ends_with( $path, '/delete' ) ) { if ( isset( $input['empty_status'] ) && $this->validate_empty_status_param( $input['empty_status'] ) ) { $result['results'] = $this->delete_all( $input['empty_status'] ); } else { $result['results'] = $this->bulk_delete_comments( $comment_ids ); } } else { $status = isset( $input['status'] ) ? $input['status'] : ''; $result['results'] = $this->bulk_update_comments_status( $comment_ids, $status ); } wp_defer_comment_counting( false ); return $result; } /** * Determine if the passed comment status is valid or not. * * @param string $status - status of passed comment. * * @return boolean */ public function validate_status_param( $status ) { return in_array( $status, array( 'approved', 'unapproved', 'pending', 'spam', 'trash' ), true ); } /** * Determine if the passed empty status is valid or not. * * @param string $empty_status - empty_status of comment. * * @return boolean */ public function validate_empty_status_param( $empty_status ) { return in_array( $empty_status, array( 'spam', 'trash' ), true ); } /** * Update the status of multiple comments. * * @param array $comment_ids Comments to update. * @param string $status New status value. * * @return array Updated comments IDs. */ public function bulk_update_comments_status( $comment_ids, $status ) { if ( count( $comment_ids ) < 1 ) { return new WP_Error( 'empty_comment_ids', 'The request must include comment_ids', 400 ); } if ( ! $this->validate_status_param( $status ) ) { return new WP_Error( 'invalid_status', "Invalid comment status value provided: '$status'.", 400 ); } $results = array(); foreach ( $comment_ids as $comment_id ) { if ( ! current_user_can( 'edit_comment', $comment_id ) ) { continue; } $result = false; switch ( $status ) { case 'approved': $result = wp_set_comment_status( $comment_id, 'approve' ); break; case 'unapproved': case 'pending': $result = wp_set_comment_status( $comment_id, 'hold' ); break; case 'spam': $result = wp_spam_comment( $comment_id ); break; case 'trash': $result = wp_trash_comment( $comment_id ); break; } if ( $result ) { $results[] = $comment_id; } } return $results; } /** * Permanenty delete multiple comments. * * Comments are only permanently deleted if trash is disabled or their status is `trash` or `spam`. * Otherwise they are moved to trash. * * @param array $comment_ids Comments to trash or delete. * * @return array Deleted comments IDs. */ public function bulk_delete_comments( $comment_ids ) { if ( count( $comment_ids ) < 1 ) { return new WP_Error( 'empty_comment_ids', 'The request must include comment_ids', 400 ); } $results = array(); foreach ( $comment_ids as $comment_id ) { if ( ! current_user_can( 'edit_comment', $comment_id ) ) { continue; } if ( wp_delete_comment( $comment_id ) ) { $results[] = $comment_id; } } return $results; } /** * Delete all spam or trash comments. * * Comments are only permanently deleted if trash is disabled or their status is `trash` or `spam`. * Otherwise they are moved to trash. * * @param string $status Can be `spam` or `trash`. * * @return array Deleted comments IDs. */ public function delete_all( $status ) { global $wpdb; // This could potentially take a long time, so we only want to delete comments created // before this operation. // Comments marked `spam` or `trash` after this moment won't be touched. // Core uses the `pagegen_timestamp` hidden field for this same reason. $delete_time = gmdate( 'Y-m-d H:i:s' ); $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_approved = %s AND %s > comment_date_gmt", $status, $delete_time ) ); if ( ! is_countable( $comment_ids ) || array() === $comment_ids ) { return array(); } return $this->bulk_delete_comments( $comment_ids ); } } class.wpcom-json-api-list-roles-endpoint.php 0000644 00000011133 14722054026 0015136 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List roles endpoint. */ new WPCOM_JSON_API_List_Roles_Endpoint( array( 'description' => 'List the user roles of a site.', 'group' => '__do_not_document', 'stat' => 'roles:list', 'max_version' => '1.1', 'method' => 'GET', 'path' => '/sites/%s/roles', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array(), 'response_format' => array( 'roles' => '(array:role) Array of role objects.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/roles', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); new WPCOM_JSON_API_List_Roles_Endpoint( array( 'description' => 'List the user roles of a site.', 'group' => '__do_not_document', 'stat' => 'roles:list', 'min_version' => '1.2', 'force' => 'wpcom', 'method' => 'GET', 'path' => '/sites/%s/roles', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array(), 'response_format' => array( 'roles' => '(array:role) Array of role objects.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/roles', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List Roles endpoint class. * * /sites/%s/roles/ -> $blog_id */ class WPCOM_JSON_API_List_Roles_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Response format. * * @var array */ public $response_format = array( 'roles' => '(array:role) Array of role objects', ); /** * Sort so roles with the most number of capabilities comes first, then the next role, and so on. * * @param object $a - the first object we're comparing. * @param object $b - the second object we're comparing. */ public static function role_sort( $a, $b ) { $core_role_names = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ); $a_is_core_role = in_array( $a->name, $core_role_names, true ); $b_is_core_role = in_array( $b->name, $core_role_names, true ); // Core roles always come before non-core roles. if ( $a_is_core_role !== $b_is_core_role ) { return $b_is_core_role <=> $a_is_core_role; } // otherwise the one with the > number of capabilities comes first. $a_cap_count = is_countable( $a->capabilities ) ? count( $a->capabilities ) : 0; $b_cap_count = is_countable( $b->capabilities ) ? count( $b->capabilities ) : 0; return $b_cap_count <=> $a_cap_count; } /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $roles = array(); $sal_site = $this->get_platform()->get_site( $blog_id ); $wp_roles = $sal_site->get_roles(); // Check if the site is connected and talks to us on a regular basis. $is_connected = $sal_site->is_connected_site(); if ( is_wp_error( $is_connected ) ) { return $is_connected; } if ( ! $sal_site->current_user_can( 'list_users' ) ) { return new WP_Error( 'unauthorized', 'User cannot view roles for specified site', 403 ); } if ( $wp_roles instanceof WP_Roles ) { $role_names = $wp_roles->get_names(); $role_keys = array_keys( $role_names ); foreach ( (array) $role_keys as $role_key ) { $role_details = get_role( $role_key ); $role_details->display_name = translate_user_role( $role_names[ $role_key ] ); $roles[] = $role_details; } } elseif ( is_array( $wp_roles ) ) { // Jetpack Shadow Site side of things. foreach ( $wp_roles as $role_key => $role ) { $roles[] = (object) array( 'name' => $role_key, 'display_name' => $role['name'], 'capabilities' => (object) $role['capabilities'], ); } } usort( $roles, array( 'self', 'role_sort' ) ); /** * Filter for curating the list of roles available for a wpcom site. * * @module json-api * * @since 8.7.0 * * @param array $roles List of role objects available to the site. */ $roles = apply_filters( 'wpcom_api_site_roles', $roles ); return array( 'roles' => $roles ); } } class.wpcom-json-api-list-media-endpoint.php 0000644 00000005546 14722054026 0015104 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List media endpoint. */ new WPCOM_JSON_API_List_Media_Endpoint( array( 'description' => 'Get a list of items in the media library.', 'group' => 'media', 'stat' => 'media', 'method' => 'GET', 'path' => '/sites/%s/media/', 'deprecated' => true, 'new_version' => '1.1', 'max_version' => '1', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'number' => '(int=20) The number of media items to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'parent_id' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.', 'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).", ), 'response_format' => array( 'media' => '(array) Array of media', 'found' => '(int) The number of total results found', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/?number=2', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * API List media endpoint class. */ class WPCOM_JSON_API_List_Media_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // upload_files can probably be used for other endpoints but we want contributors to be able to use media too. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $args = $this->query_args(); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } $media = get_posts( array( 'post_type' => 'attachment', 'post_parent' => $args['parent_id'], 'offset' => $args['offset'], 'numberposts' => $args['number'], 'post_mime_type' => $args['mime_type'], ) ); $response = array(); foreach ( $media as $item ) { $response[] = $this->get_media_item( $item->ID ); } $_num = (array) wp_count_attachments(); $_total_media = array_sum( $_num ) - $_num['trash']; $return = array( 'found' => $_total_media, 'media' => $response, ); return $return; } } class.wpcom-json-api-render-embed-reversal-endpoint.php 0000644 00000013140 14722054026 0017213 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Render_Embed_Reversal_Endpoint( array( 'description' => 'Determines if the given embed code can be reversed into a single line embed or a shortcode, and if so returns the embed or shortcode. Note: The current user must have publishing access.', 'group' => '__do_not_document', 'stat' => 'embeds:reversal', 'method' => 'POST', 'path' => '/sites/%s/embeds/reversal', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'maybe_embed' => '(string) The embed code to reverse. Required. Only accepts one at a time.', ), 'response_format' => array( 'maybe_embed' => '(string) The original embed code that was passed in for rendering.', 'reversal_type' => '(string) The type of reversal. Either an embed or a shortcode.', 'render_result' => '(html) The rendered HTML result of the embed or shortcode.', 'result' => '(string) The reversed content. Either a single line embed or a shortcode.', 'scripts' => '(array) An array of JavaScript files needed to render the embed or shortcode. Returned in the format of <code>{ "script-slug" : { "src": "http://example.com/file.js", "extra" : "" } }</code> where extra contains any neccessary extra JS for initializing the source file and src contains the script to load. Omitted if no scripts are neccessary.', 'styles' => '(array) An array of CSS files needed to render the embed or shortcode. Returned in the format of <code>{ "style-slug" : { "src": "http://example.com/file.css", "media" : "all" } }</code>. Omitted if no styles are neccessary.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcode-reversals/render/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'maybe_embed' => '<iframe width="480" height="302" src="http://www.ustream.tv/embed/recorded/26370522/highlight/299667?v=3&wmode=direct" scrolling="no" frameborder="0"></iframe>', ), ), ) ); /** * Render embed reversal class. * * /sites/%s/embeds/reversal -> $blog_id */ class WPCOM_JSON_API_Render_Embed_Reversal_Endpoint extends WPCOM_JSON_API_Render_Endpoint { /** * API callback. * * @param string $path - the path (unused). * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 ); } $is_embed = false; $is_shortcode = false; $input = $this->input( true ); $maybe_embed = trim( $input['maybe_embed'] ); if ( empty( $maybe_embed ) ) { return new WP_Error( 'empty_embed', 'Please provide an embed code to process.', 400 ); } $ksesed_content = trim( wp_strip_all_tags( wp_kses_post( $maybe_embed ), true ) ); if ( empty( $ksesed_content ) ) { return new WP_Error( 'invalid_embed', 'Invalid or empty embed provided.', 400 ); } $shortcode_pattern = get_shortcode_regex(); $url_pattern = '/^http(s)?:\/\/[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(\/.*)?$/i'; preg_match_all( "/$shortcode_pattern/s", $ksesed_content, $shortcode_matches ); preg_match_all( "$url_pattern", $ksesed_content, $url_matches ); if ( empty( $shortcode_matches[0] ) && empty( $url_matches[0] ) ) { return new WP_Error( 'invalid_embed', 'The provided embed is not supported.', 400 ); } $shortcode_matches_count = is_countable( $shortcode_matches[0] ) ? count( $shortcode_matches[0] ) : 0; $url_matches_count = is_countable( $url_matches[0] ) ? count( $url_matches[0] ) : 0; if ( ( $shortcode_matches_count + $url_matches_count ) > 1 ) { return new WP_Error( 'invalid_embed', 'Only one embed/shortcode reversal can be rendered at a time.', 400 ); } if ( ! empty( $shortcode_matches[0] ) ) { $is_shortcode = true; } elseif ( ! empty( $url_matches[0] ) ) { $is_embed = true; } $render = $this->process_render( array( $this, 'render_shortcode_reversal' ), array( 'shortcode_reversal' => $ksesed_content, 'is_shortcode' => $is_shortcode, 'is_embed' => $is_embed, ) ); // if nothing happened, then the shortcode does not exist. global $wp_embed; if ( empty( $render ) || empty( $render['result'] ) || $ksesed_content === $render['result'] || $wp_embed->maybe_make_link( $maybe_embed ) === $render['result'] ) { return new WP_Error( 'invalid_embed', 'The provided embed is not supported.', 400 ); } // our output for this endpoint.. $return = array(); $return['maybe_embed'] = $maybe_embed; $return['result'] = $ksesed_content; $return['reversal_type'] = ( $is_embed ) ? 'embed' : 'shortcode'; $return['render_result'] = $render['result']; $return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] ); return $return; } /** * Render the shortcode reversal. * * @param array $args - the arguments. * * @return mixed|false */ public function render_shortcode_reversal( $args ) { if ( $args['is_shortcode'] ) { return $this->do_shortcode( $args['shortcode_reversal'] ); } elseif ( $args['is_embed'] ) { return $this->do_embed( $args['shortcode_reversal'] ); } return false; } } class.wpcom-json-api-taxonomy-endpoint.php 0000644 00000002732 14722054026 0014724 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Taxonomy endpoint. */ abstract class WPCOM_JSON_API_Taxonomy_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Category object format. * * @var array */ public $category_object_format = array( 'ID' => '(int) The category ID.', 'name' => '(string) The name of the category.', 'slug' => '(string) The slug of the category.', 'description' => '(string) The description of the category.', 'post_count' => '(int) The number of posts using this category.', 'feed_url' => '(string) The URL of the feed for this category.', 'parent' => '(int) The parent ID for the category.', 'meta' => '(object) Meta data', ); /** * Tag object format. * * @var array */ public $tag_object_format = array( 'ID' => '(int) The tag ID.', 'name' => '(string) The name of the tag.', 'slug' => '(string) The slug of the tag.', 'description' => '(string) The description of the tag.', 'post_count' => '(int) The number of posts using this t.', 'meta' => '(object) Meta data', ); /** * Constructor function. * * @param string|array|object $args - the arguments. */ public function __construct( $args ) { parent::__construct( $args ); if ( preg_match( '#/tags/#i', $this->path ) ) { $this->response_format =& $this->tag_object_format; } else { $this->response_format =& $this->category_object_format; } } } class.wpcom-json-api-render-embed-endpoint.php 0000644 00000006250 14722054026 0015376 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName new WPCOM_JSON_API_Render_Embed_Endpoint( array( 'description' => 'Get a rendered embed for a site. Note: The current user must have publishing access.', 'group' => 'sites', 'stat' => 'embeds:render', 'method' => 'GET', 'path' => '/sites/%s/embeds/render', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'embed_url' => '(string) The query-string encoded embed URL to render. Required. Only accepts one at a time.', ), 'response_format' => array( 'embed_url' => '(string) The embed_url that was passed in for rendering.', 'result' => '(html) The rendered HTML result of the embed.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/apiexamples.wordpress.com/embeds/render?embed_url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSQEQr7c0-dw', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * Render embed endpoint class. * * /sites/%s/embeds/render -> $blog_id */ class WPCOM_JSON_API_Render_Embed_Endpoint extends WPCOM_JSON_API_Render_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', __( 'Your token must have permission to post on this blog.', 'jetpack' ), 403 ); } $args = $this->query_args(); $embed_url = trim( $args['embed_url'] ); // quick validation if ( ! preg_match_all( '|^\s*(https?://[^\s"]+)\s*$|im', $embed_url, $matches ) ) { return new WP_Error( 'invalid_embed_url', __( 'The embed_url parameter must be a valid URL.', 'jetpack' ), 400 ); } if ( is_countable( $matches[1] ) && count( $matches[1] ) > 1 ) { return new WP_Error( 'invalid_embed', __( 'Only one embed can be rendered at a time.', 'jetpack' ), 400 ); } $embed_url = array_shift( $matches[1] ); $parts = wp_parse_url( $embed_url ); if ( ! $parts ) { return new WP_Error( 'invalid_embed_url', __( 'The embed_url parameter must be a valid URL.', 'jetpack' ), 400 ); } global $wp_embed; $render = $this->process_render( array( $this, 'do_embed' ), $embed_url ); // if nothing happened, then the shortcode does not exist. $is_an_embed = ( $embed_url !== $render['result'] && $wp_embed->maybe_make_link( $embed_url ) !== $render['result'] ); if ( ! $is_an_embed ) { return new WP_Error( 'invalid_embed', __( 'The requested URL is not an embed.', 'jetpack' ), 400 ); } // our output for this endpoint.. $return = array(); $return['embed_url'] = $embed_url; $return['result'] = $render['result']; $return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] ); return $return; } } class.wpcom-json-api-list-posts-endpoint.php 0000644 00000031333 14722054026 0015166 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List posts endpoint. */ new WPCOM_JSON_API_List_Posts_Endpoint( array( 'description' => 'Get a list of matching posts.', 'new_version' => '1.1', 'max_version' => '1', 'group' => 'posts', 'stat' => 'posts', 'method' => 'GET', 'path' => '/sites/%s/posts/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'number' => '(int=20) The number of posts to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.', 'order' => array( 'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.', 'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.', ), 'order_by' => array( 'date' => 'Order by the created time of each post.', 'modified' => 'Order by the modified time of each post.', 'title' => "Order lexicographically by the posts' titles.", 'comment_count' => 'Order by the number of comments for each post.', 'ID' => 'Order by post ID.', ), 'after' => '(ISO 8601 datetime) Return posts dated on or after the specified datetime.', 'before' => '(ISO 8601 datetime) Return posts dated on or before the specified datetime.', 'tag' => '(string) Specify the tag name or slug.', 'category' => '(string) Specify the category name or slug.', 'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.', 'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.', 'include' => '(array:int|int) Includes the specified post ID(s) in the response', 'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response', 'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.', 'status' => array( 'publish' => 'Return only published posts.', 'private' => 'Return only private posts.', 'draft' => 'Return only draft posts.', 'pending' => 'Return only posts pending editorial approval.', 'future' => 'Return only posts scheduled for future publishing.', 'trash' => 'Return only posts in the trash.', 'any' => 'Return all posts regardless of status.', ), 'sticky' => array( 'false' => 'Post is not marked as sticky.', 'true' => 'Stick the post to the front page.', ), 'author' => "(int) Author's user ID", 'search' => '(string) Search query', 'meta_key' => '(string) Metadata key that the post should contain', 'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/?number=5', ) ); /** * List posts endpoint class. * * /sites/%s/posts/ -> $blog_id */ class WPCOM_JSON_API_List_Posts_Endpoint extends WPCOM_JSON_API_Post_Endpoint { /** * The date range. * * @var array */ public $date_range = array(); /** * The response format. * * @var array */ public $response_format = array( 'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).', 'posts' => '(array:post) An array of post objects.', ); /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } if ( isset( $args['type'] ) && ! $this->is_post_type_allowed( $args['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } // Normalize post_type. if ( isset( $args['type'] ) && 'any' === $args['type'] ) { if ( version_compare( $this->api->version, '1.1', '<' ) ) { $args['type'] = array( 'post', 'page' ); } else { // 1.1+ $args['type'] = $this->_get_whitelisted_post_types(); } } // determine statuses. $status = $args['status']; $status = ( $status ) ? explode( ',', $status ) : array( 'publish' ); if ( is_user_logged_in() ) { $statuses_whitelist = array( 'publish', 'pending', 'draft', 'future', 'private', 'trash', 'any', ); $status = array_intersect( $status, $statuses_whitelist ); } else { // logged-out users can see only published posts. $statuses_whitelist = array( 'publish', 'any' ); $status = array_intersect( $status, $statuses_whitelist ); if ( empty( $status ) ) { // requested only protected statuses? nothing for you here. return array( 'found' => 0, 'posts' => array(), ); } // clear it (AKA published only) because "any" includes protected. $status = array(); } // let's be explicit about defaulting to 'post'. $args['type'] = isset( $args['type'] ) ? $args['type'] : 'post'; // make sure the user can read or edit the requested post type(s). if ( is_array( $args['type'] ) ) { $allowed_types = array(); foreach ( $args['type'] as $post_type ) { if ( $this->current_user_can_access_post_type( $post_type, $args['context'] ) ) { $allowed_types[] = $post_type; } } if ( empty( $allowed_types ) ) { return array( 'found' => 0, 'posts' => array(), ); } $args['type'] = $allowed_types; } elseif ( ! $this->current_user_can_access_post_type( $args['type'], $args['context'] ) ) { return array( 'found' => 0, 'posts' => array(), ); } $query = array( 'posts_per_page' => $args['number'], 'order' => $args['order'], 'orderby' => $args['order_by'], 'post_type' => $args['type'], 'post_status' => $status, 'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null, 'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null, 's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null, 'fields' => 'ids', ); if ( ! is_user_logged_in() ) { $query['has_password'] = false; } if ( isset( $args['include'] ) ) { $query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] ); } if ( isset( $args['meta_key'] ) ) { $show = false; if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) { $show = true; } if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) { $show = true; } if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) { return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 ); } $meta = array( 'key' => $args['meta_key'] ); if ( isset( $args['meta_value'] ) ) { $meta['value'] = $args['meta_value']; } $query['meta_query'] = array( $meta ); } $sticky = get_option( 'sticky_posts' ); if ( isset( $args['sticky'] ) && $sticky && is_array( $sticky ) ) { if ( $args['sticky'] ) { $query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky; } else { $query['post__not_in'] = $sticky; $query['ignore_sticky_posts'] = 1; } } else { $query['post__not_in'] = $sticky; $query['ignore_sticky_posts'] = 1; } if ( isset( $args['exclude'] ) ) { $query['post__not_in'] = array_merge( $query['post__not_in'], (array) $args['exclude'] ); } if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) { // get_page_children is a misnomer; it supports all hierarchical post types. $page_args = array( 'child_of' => $args['exclude_tree'], 'post_type' => $args['type'], // since we're looking for things to exclude, be aggressive. 'post_status' => 'publish,draft,pending,private,future,trash', ); $post_descendants = get_pages( $page_args ); $exclude_tree = array( $args['exclude_tree'] ); foreach ( $post_descendants as $child ) { $exclude_tree[] = $child->ID; } $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree; } if ( isset( $args['category'] ) ) { $category = get_term_by( 'slug', $args['category'], 'category' ); if ( false === $category ) { $query['category_name'] = $args['category']; } else { $query['cat'] = $category->term_id; } } if ( isset( $args['tag'] ) ) { $query['tag'] = $args['tag']; } if ( ! empty( $args['term'] ) ) { $query['tax_query'] = array(); foreach ( $args['term'] as $taxonomy => $slug ) { $taxonomy_object = get_taxonomy( $taxonomy ); if ( false === $taxonomy_object || ( ! $taxonomy_object->public && ! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) { continue; } $query['tax_query'][] = array( 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => explode( ',', $slug ), ); } } if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $query['paged'] = $args['page']; } else { if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $query['offset'] = $args['offset']; } if ( isset( $args['before'] ) ) { $this->date_range['before'] = $args['before']; } if ( isset( $args['after'] ) ) { $this->date_range['after'] = $args['after']; } if ( $this->date_range ) { add_filter( 'posts_where', array( $this, 'handle_date_range' ) ); } /** * 'column' necessary for the me/posts endpoint (which extends sites/$site/posts). * Would need to be added to the sites/$site/posts definition if we ever want to * use it there. */ $column_whitelist = array( 'post_modified_gmt' ); if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) { $query['column'] = $args['column']; } $wp_query = new WP_Query( $query ); if ( $this->date_range ) { remove_filter( 'posts_where', array( $this, 'handle_date_range' ) ); $this->date_range = array(); } $return = array(); $excluded_count = 0; foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $return[ $key ] = (int) $wp_query->found_posts; break; case 'posts': $posts = array(); foreach ( $wp_query->posts as $post_ID ) { $the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] ); if ( $the_post && ! is_wp_error( $the_post ) ) { $posts[] = $the_post; } else { ++$excluded_count; } } if ( $posts ) { /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) ); } $return[ $key ] = $posts; break; } } $return['found'] -= $excluded_count; return $return; } /** * Handle the date range. * * @param string $where - SQL where clause. */ public function handle_date_range( $where ) { global $wpdb; switch ( count( $this->date_range ) ) { case 2: $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ", $this->date_range['after'], $this->date_range['before'] ); break; case 1: if ( isset( $this->date_range['before'] ) ) { $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date <= CAST( %s AS DATETIME ) ", $this->date_range['before'] ); } else { $where .= $wpdb->prepare( " AND `$wpdb->posts`.post_date >= CAST( %s AS DATETIME ) ", $this->date_range['after'] ); } break; } return $where; } } class.wpcom-json-api-bulk-delete-post-endpoint.php 0000644 00000004465 14722054026 0016233 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Bulk delete posts on a site. * * Endpoint: /sites/%s/posts/delete */ new WPCOM_JSON_API_Bulk_Delete_Post_Endpoint( array( 'description' => 'Delete multiple posts. Note: If the trash is enabled, this request will send non-trashed posts to the trash. Trashed posts will be permanently deleted.', 'group' => 'posts', 'stat' => 'posts:1:bulk-delete', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/posts/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'post_ids' => '(array|string) An array, or comma-separated list, of Post IDs to delete or trash.', ), 'response_format' => array( 'results' => '(object) An object containing results, ', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'post_ids' => array( 881, 882 ), ), ), ) ); /** * Bulk delete post endpoint class. */ class WPCOM_JSON_API_Bulk_Delete_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $post_id - the post ID. */ public function callback( $path = '', $blog_id = 0, $post_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $input = $this->input(); if ( is_array( $input['post_ids'] ) ) { $post_ids = (array) $input['post_ids']; } elseif ( ! empty( $input['post_ids'] ) ) { $post_ids = explode( ',', $input['post_ids'] ); } else { $post_ids = array(); } if ( count( $post_ids ) < 1 ) { return new WP_Error( 'empty_post_ids', 'The request must include post_ids' ); } $result = array( 'results' => array(), ); foreach ( $post_ids as $post_id ) { $result['results'][ $post_id ] = $this->delete_post( $path, $blog_id, $post_id ); } return $result; } } class.wpcom-json-api-update-media-v1-1-endpoint.php 0000644 00000016503 14722054026 0016070 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update media item info v1.1 endpoint. * * Endpoint: v1.1/sites/%s/media/%d */ new WPCOM_JSON_API_Update_Media_v1_1_Endpoint( array( 'description' => 'Edit basic information about a media item.', 'group' => 'media', 'stat' => 'media:1:POST', 'min_version' => '1.1', 'max_version' => '1.1', 'method' => 'POST', 'path' => '/sites/%s/media/%d', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$media_ID' => '(int) The ID of the media item', ), 'request_format' => array( 'parent_id' => '(int) ID of the post this media is attached to', 'title' => '(string) The file name.', 'caption' => '(string) File caption.', 'description' => '(HTML) Description of the file.', 'alt' => '(string) Alternative text for image files.', 'rating' => '(string) Video only. Video rating.', 'display_embed' => '(string) Video only. Whether to share or not the video.', 'allow_download' => '(string) Video only. Whether the video can be downloaded or not.', 'privacy_setting' => '(int) Video only. The privacy level for the video.', 'artist' => '(string) Audio Only. Artist metadata for the audio track.', 'album' => '(string) Audio Only. Album metadata for the audio track.', ), 'response_format' => array( 'ID' => '(int) The ID of the media item', 'date' => '(ISO 8601 datetime) The date the media was uploaded', 'post_ID' => '(int) ID of the post this media is attached to', 'author_ID' => '(int) ID of the user who uploaded the media', 'URL' => '(string) URL to the file', 'guid' => '(string) Unique identifier', 'file' => '(string) File name', 'extension' => '(string) File extension', 'mime_type' => '(string) File mime type', 'title' => '(string) File name', 'caption' => '(string) User provided caption of the file', 'description' => '(string) Description of the file', 'alt' => '(string) Alternative text for image files.', 'thumbnails' => '(object) Media item thumbnail URL options', 'height' => '(int) (Image & video only) Height of the media item', 'width' => '(int) (Image & video only) Width of the media item', 'length' => '(int) (Video & audio only) Duration of the media item, in seconds', 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item', 'rating' => '(string) (Video only) VideoPress rating of the video', 'display_embed' => '(string) Video only. Whether to share or not the video.', 'allow_download' => '(string) Video only. Whether the video can be downloaded or not.', 'privacy_setting' => '(int) Video only. The privacy level for the video.', 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress', 'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/446', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'title' => 'Updated Title', ), ), ) ); // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid /** * Update media item info v1.1 class. */ class WPCOM_JSON_API_Update_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Update media item info API v1.1 callback. * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $media_id Media ID. * * @return object|WP_Error */ public function callback( $path = '', $blog_id = 0, $media_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'upload_files', $media_id ) ) { return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); } $item = $this->get_media_item_v1_1( $media_id ); if ( is_wp_error( $item ) ) { return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); } $input = $this->input( true ); $insert = array(); if ( isset( $input['title'] ) ) { $insert['post_title'] = $input['title']; } if ( isset( $input['caption'] ) ) { $insert['post_excerpt'] = $input['caption']; } if ( isset( $input['description'] ) ) { $insert['post_content'] = $input['description']; } if ( isset( $input['parent_id'] ) ) { $insert['post_parent'] = $input['parent_id']; } if ( isset( $input['alt'] ) ) { $alt = wp_strip_all_tags( $input['alt'], true ); update_post_meta( $media_id, '_wp_attachment_image_alt', $alt ); } // audio only artist/album info. if ( str_starts_with( $item->mime_type, 'audio/' ) ) { $changed = false; $id3data = wp_get_attachment_metadata( $media_id ); if ( ! is_array( $id3data ) ) { $changed = true; $id3data = array(); } $id3_keys = array( 'artist' => __( 'Artist', 'jetpack' ), 'album' => __( 'Album', 'jetpack' ), ); foreach ( $id3_keys as $key => $label ) { if ( isset( $input[ $key ] ) ) { $changed = true; $id3data[ $key ] = wp_strip_all_tags( $input[ $key ], true ); } } if ( $changed ) { wp_update_attachment_metadata( $media_id, $id3data ); } } // Pass the item to the handle_video_meta() that checks if it's a VideoPress item and saves it. $result = $this->handle_video_meta( $media_id, $input, $item ); if ( is_wp_error( $result ) ) { return $result; } $insert['ID'] = $media_id; wp_update_post( (object) $insert ); $item = $this->get_media_item_v1_1( $media_id ); return $item; } /** * Persist the VideoPress metadata if the given item argument is a VideoPress item. * * @param string $media_id The ID of the video. * @param array $input The request input. * @param stdClass $item The response item. * * @return bool|WP_Error */ public function handle_video_meta( $media_id, $input, $item ) { if ( ! class_exists( \Videopress_Attachment_Metadata::class ) ) { return false; } if ( ! \Videopress_Attachment_Metadata::is_videopress_media( $item ) ) { return false; } return \Videopress_Attachment_Metadata::persist_metadata( $media_id, $item->videopress_guid, isset( $input['title'] ) ? $input['title'] : null, isset( $input['caption'] ) ? $input['caption'] : null, isset( $input['description'] ) ? $input['description'] : null, isset( $input['rating'] ) ? $input['rating'] : null, isset( $input['display_embed'] ) ? $input['display_embed'] : null, isset( $input['allow_download'] ) ? $input['allow_download'] : null, isset( $input['privacy_setting'] ) ? $input['privacy_setting'] : null ); } } class.wpcom-json-api-list-posts-v1-2-endpoint.php 0000644 00000041267 14722054026 0015660 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List posts v1_2 endpoint. */ new WPCOM_JSON_API_List_Posts_v1_2_Endpoint( array( 'description' => 'Get a list of matching posts.', 'min_version' => '1.2', 'max_version' => '1.2', 'group' => 'posts', 'stat' => 'posts', 'method' => 'GET', 'path' => '/sites/%s/posts/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'allow_fallback_to_jetpack_blog_token' => true, 'query_parameters' => array( 'number' => '(int=20) The number of posts to return. Limit: 100.', 'offset' => '(int=0) 0-indexed offset.', 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.', 'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.', 'order' => array( 'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.', 'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.', ), 'order_by' => array( 'date' => 'Order by the created time of each post.', 'modified' => 'Order by the modified time of each post.', 'title' => "Order lexicographically by the posts' titles.", 'comment_count' => 'Order by the number of comments for each post.', 'ID' => 'Order by post ID.', ), 'after' => '(ISO 8601 datetime) Return posts dated after the specified datetime.', 'before' => '(ISO 8601 datetime) Return posts dated before the specified datetime.', 'modified_after' => '(ISO 8601 datetime) Return posts modified after the specified datetime.', 'modified_before' => '(ISO 8601 datetime) Return posts modified before the specified datetime.', 'tag' => '(string) Specify the tag name or slug.', 'category' => '(string) Specify the category name or slug.', 'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.', 'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.", 'exclude_private_types' => '(bool=false) Use this flag together with `type=any` to get only publicly accessible posts.', 'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.', 'include' => '(array:int|int) Includes the specified post ID(s) in the response', 'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response', 'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.', 'status' => '(string) Comma-separated list of statuses for which to query, including any of: "publish", "private", "draft", "pending", "future", and "trash", or simply "any". Defaults to "publish"', 'sticky' => array( 'include' => 'Sticky posts are not excluded from the list.', 'exclude' => 'Sticky posts are excluded from the list.', 'require' => 'Only include sticky posts', ), 'author' => "(int) Author's user ID", 'search' => '(string) Search query', 'meta_key' => '(string) Metadata key that the post should contain', 'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/posts/?number=2', ) ); /** * List posts v1_2 endpoint. * * /sites/%s/posts/ -> $blog_id */ class WPCOM_JSON_API_List_Posts_v1_2_Endpoint extends WPCOM_JSON_API_List_Posts_v1_1_Endpoint { // phpcs:ignore /** * API callback. * * @param string $path - the path. * @param string $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $args = $this->query_args(); $is_eligible_for_page_handle = true; $site = $this->get_platform()->get_site( $blog_id ); if ( $args['number'] < 1 ) { $args['number'] = 20; } elseif ( 100 < $args['number'] ) { return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); } if ( isset( $args['type'] ) ) { // load all types on WPCOM, unless only built-in ones are requested. if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! in_array( $args['type'], array( 'post', 'revision', 'page' ), true ) ) { $this->load_theme_functions(); } if ( ! $site->is_post_type_allowed( $args['type'] ) ) { return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); } // Normalize post_type. if ( 'any' === $args['type'] ) { $whitelisted_post_types = $site->get_whitelisted_post_types(); if ( ! empty( $args['exclude_private_types'] ) ) { $public_post_types = get_post_types( array( 'public' => true ) ); $args['type'] = array_intersect( $public_post_types, $whitelisted_post_types ); } else { $args['type'] = $whitelisted_post_types; } } } else { // let's be explicit about defaulting to 'post'. $args['type'] = 'post'; } // make sure the user can read or edit the requested post type(s). if ( is_array( $args['type'] ) ) { $allowed_types = array(); foreach ( $args['type'] as $post_type ) { if ( $site->current_user_can_access_post_type( $post_type, $args['context'] ) ) { $allowed_types[] = $post_type; } } if ( empty( $allowed_types ) ) { return array( 'found' => 0, 'posts' => array(), ); } $args['type'] = $allowed_types; } elseif ( ! $site->current_user_can_access_post_type( $args['type'], $args['context'] ) ) { return array( 'found' => 0, 'posts' => array(), ); } // determine statuses. $status = ( ! empty( $args['status'] ) ) ? explode( ',', $args['status'] ) : array( 'publish' ); if ( is_user_logged_in() ) { $statuses_whitelist = array( 'publish', 'pending', 'draft', 'future', 'private', 'trash', 'any', ); $status = array_intersect( $status, $statuses_whitelist ); } else { // logged-out users can see only published posts. $statuses_whitelist = array( 'publish', 'any' ); $status = array_intersect( $status, $statuses_whitelist ); if ( empty( $status ) ) { // requested only protected statuses? nothing for you here. return array( 'found' => 0, 'posts' => array(), ); } // clear it (AKA published only) because "any" includes protected. $status = array(); } $query = array( 'posts_per_page' => $args['number'], 'order' => $args['order'], 'orderby' => $args['order_by'], 'post_type' => $args['type'], 'post_status' => $status, 'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null, 'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null, 's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null, 'fields' => 'ids', ); if ( ! is_user_logged_in() ) { $query['has_password'] = false; } if ( isset( $args['include'] ) ) { $query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] ); } if ( isset( $args['meta_key'] ) ) { $show = false; if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) { $show = true; } if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) { $show = true; } if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) { return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 ); } $meta = array( 'key' => $args['meta_key'] ); if ( isset( $args['meta_value'] ) ) { $meta['value'] = $args['meta_value']; } $query['meta_query'] = array( $meta ); } if ( 'include' === $args['sticky'] ) { $query['ignore_sticky_posts'] = 1; } elseif ( 'exclude' === $args['sticky'] ) { $sticky = get_option( 'sticky_posts' ); if ( is_array( $sticky ) ) { $query['post__not_in'] = $sticky; } } elseif ( 'require' === $args['sticky'] ) { $sticky = get_option( 'sticky_posts' ); if ( is_array( $sticky ) && ! empty( $sticky ) ) { $query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky; } else { // no sticky posts exist. return array( 'found' => 0, 'posts' => array(), ); } } if ( isset( $args['exclude'] ) ) { $excluded_ids = (array) $args['exclude']; $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids; } if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) { // get_page_children is a misnomer; it supports all hierarchical post types. $page_args = array( 'child_of' => $args['exclude_tree'], 'post_type' => $args['type'], // since we're looking for things to exclude, be aggressive. 'post_status' => 'publish,draft,pending,private,future,trash', ); $post_descendants = get_pages( $page_args ); $exclude_tree = array( $args['exclude_tree'] ); foreach ( $post_descendants as $child ) { $exclude_tree[] = $child->ID; } $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree; } if ( isset( $args['category'] ) ) { $category = get_term_by( 'slug', $args['category'], 'category' ); if ( false === $category ) { $query['category_name'] = $args['category']; } else { $query['cat'] = $category->term_id; } } if ( isset( $args['tag'] ) ) { $query['tag'] = $args['tag']; } if ( ! empty( $args['term'] ) ) { $query['tax_query'] = array(); foreach ( $args['term'] as $taxonomy => $slug ) { $taxonomy_object = get_taxonomy( $taxonomy ); if ( false === $taxonomy_object || ( ! $taxonomy_object->public && ! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) { continue; } $query['tax_query'][] = array( 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => explode( ',', $slug ), ); } } if ( isset( $args['page'] ) ) { if ( $args['page'] < 1 ) { $args['page'] = 1; } $query['paged'] = $args['page']; if ( 1 !== $query['paged'] ) { $is_eligible_for_page_handle = false; } } else { if ( $args['offset'] < 0 ) { $args['offset'] = 0; } $query['offset'] = $args['offset']; if ( 0 !== $query['offset'] ) { $is_eligible_for_page_handle = false; } } if ( isset( $args['before'] ) ) { $this->date_range['before'] = $args['before']; } if ( isset( $args['after'] ) ) { $this->date_range['after'] = $args['after']; } if ( isset( $args['modified_before_gmt'] ) ) { $this->modified_range['before'] = $args['modified_before_gmt']; } if ( isset( $args['modified_after_gmt'] ) ) { $this->modified_range['after'] = $args['modified_after_gmt']; } if ( $this->date_range ) { add_filter( 'posts_where', array( $this, 'handle_date_range' ) ); } if ( $this->modified_range ) { add_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); } if ( isset( $args['page_handle'] ) ) { $page_handle = wp_parse_args( $args['page_handle'] ); if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) { // we have a valid looking page handle. $this->page_handle = $page_handle; add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } } /** * 'column' necessary for the me/posts endpoint (which extends sites/$site/posts). * Would need to be added to the sites/$site/posts definition if we ever want to * use it there. */ $column_whitelist = array( 'post_modified_gmt' ); if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) { $query['column'] = $args['column']; } $this->performed_query = $query; add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); $wp_query = new WP_Query( $query ); remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); if ( $this->date_range ) { remove_filter( 'posts_where', array( $this, 'handle_date_range' ) ); $this->date_range = array(); } if ( $this->modified_range ) { remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); $this->modified_range = array(); } if ( $this->page_handle ) { remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); } $return = array(); $excluded_count = 0; foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $return[ $key ] = (int) $wp_query->found_posts; break; case 'posts': $posts = array(); foreach ( $wp_query->posts as $post_ID ) { $the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] ); if ( $the_post && ! is_wp_error( $the_post ) ) { $posts[] = $the_post; } else { ++$excluded_count; } } if ( $posts ) { /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) ); } $return[ $key ] = $posts; break; case 'meta': if ( ! is_array( $args['type'] ) ) { $return[ $key ] = (object) array( 'links' => (object) array( 'counts' => (string) $this->links->get_site_link( $blog_id, 'post-counts/' . $args['type'] ), ), ); } if ( $is_eligible_for_page_handle && $return['posts'] ) { $last_post = end( $return['posts'] ); reset( $return['posts'] ); $post_count = is_countable( $return['posts'] ) ? count( $return['posts'] ) : 0; if ( ( $return['found'] > $post_count ) && $last_post ) { if ( ! isset( $return[ $key ] ) ) { $return[ $key ] = (object) array(); } $handle = $this->build_page_handle( $last_post, $query ); if ( $handle !== null ) { $return[ $key ]->next_page = $handle; } } } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( ! isset( $return[ $key ] ) ) { $return[ $key ] = new stdClass(); } $return[ $key ]->wpcom = true; } break; } } $return['found'] -= $excluded_count; return $return; } /** * Build page handle. * * @param array $post - the post. * @param array $query - the query. */ public function build_page_handle( $post, $query ) { $column = $query['orderby']; if ( ! $column ) { $column = 'date'; } if ( ! isset( $post['ID'] ) || ! isset( $post[ $column ] ) ) { return null; } return build_query( array( 'value' => rawurlencode( $post[ $column ] ), 'id' => $post['ID'], ) ); } /** * Build the date range query. * * @param string $column - the database column. * @param array $range - the date range. * @param string $where - sql where clause. */ public function build_date_range_query( $column, $range, $where ) { global $wpdb; switch ( count( $range ) ) { case 2: $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['after'], $range['before'] ); break; case 1: if ( isset( $range['before'] ) ) { $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['before'] ); } else { $where .= $wpdb->prepare( " AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $range['after'] ); } break; } return $where; } } class.wpcom-json-api-update-customcss.php 0000644 00000005565 14722054026 0014542 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Custom CSS update endpoint. * * Endpoint: /sites/%s/customcss */ new WPCOM_JSON_API_Update_CustomCss_Endpoint( array( 'description' => 'Set custom-css data for a site.', 'group' => '__do_not_document', 'stat' => 'customcss:1:update', 'method' => 'POST', 'min_version' => '1.1', 'path' => '/sites/%s/customcss', 'path_labels' => array( '$site' => '(string) Site ID or domain.', ), 'request_format' => array( 'css' => '(string) Optional. The raw CSS.', 'preprocessor' => '(string) Optional. The name of the preprocessor if any.', 'add_to_existing' => '(bool) Optional. False to skip the existing styles.', ), 'response_format' => array( 'css' => '(string) The raw CSS.', 'preprocessor' => '(string) The name of the preprocessor if any.', 'add_to_existing' => '(bool) False to skip the existing styles.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/customcss', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), 'body' => array( 'css' => '.stie-title { color: #fff; }', 'preprocessor' => 'sass', ), ), 'example_response' => ' { "css": ".site-title { color: #fff; }", "preprocessor": "sass", "add_to_existing": "true" }', ) ); /** * Custom CSS update endpoint class. */ class WPCOM_JSON_API_Update_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Custom CSS update endpoint API callback. * * @param string $path API path. * @param int $blog_id Blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { // Switch to the given blog. $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( 'unauthorized', 'User is not authorized to access custom css', 403 ); } $args = $this->input(); if ( empty( $args ) || ! is_array( $args ) ) { return new WP_Error( 'no_data', 'No data was provided.', 400 ); } $save_args = array( 'css' => $args['css'], 'preprocessor' => $args['preprocessor'], 'add_to_existing' => $args['add_to_existing'], ); Jetpack_Custom_CSS::save( $save_args ); $current = array( 'css' => Jetpack_Custom_CSS::get_css(), 'preprocessor' => Jetpack_Custom_CSS::get_preprocessor_key(), 'add_to_existing' => ! Jetpack_Custom_CSS::skip_stylesheet(), ); $defaults = array( 'css' => '', 'preprocessor' => '', 'add_to_existing' => true, ); return wp_parse_args( $current, $defaults ); } } class.wpcom-json-api-list-embeds-endpoint.php 0000644 00000004053 14722054026 0015254 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * List Embeds endpoint. */ new WPCOM_JSON_API_List_Embeds_Endpoint( array( 'description' => 'Get a list of embeds available on a site. Note: The current user must have publishing access.', 'group' => 'sites', 'stat' => 'embeds', 'method' => 'GET', 'path' => '/sites/%s/embeds', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'response_format' => array( 'embeds' => '(array) A list of supported embeds by their regex pattern.', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/embeds', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), ) ); /** * List Embeds Endpoint class. * * /sites/%s/embeds -> $blog_id */ class WPCOM_JSON_API_List_Embeds_Endpoint extends WPCOM_JSON_API_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } // permissions check. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 ); } // list em. $output = array( 'embeds' => array() ); if ( ! function_exists( '_wp_oembed_get_object' ) ) { require_once ABSPATH . WPINC . '/class-oembed.php'; } global $wp_embed; $oembed = _wp_oembed_get_object(); foreach ( $wp_embed->handlers as $handlers ) { foreach ( $handlers as $handler ) { if ( ! empty( $handler['regex'] ) ) { $output['embeds'][] = $handler['regex']; } } } foreach ( $oembed->providers as $regex => $oembed_info ) { if ( ! empty( $regex ) ) { $output['embeds'][] = $regex; } } return $output; } } class.wpcom-json-api-get-comment-counts-endpoint.php 0000644 00000006064 14722054026 0016600 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Endpoint: /sites/%s/comment-counts */ new WPCOM_JSON_API_GET_Comment_Counts_Endpoint( array( 'description' => 'Get comment counts for each available status', 'group' => 'comments', 'stat' => 'comments:1:comment-counts', 'method' => 'GET', 'path' => '/sites/%s/comment-counts', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'post_id' => '(int) post ID for filtering the comment counts by post', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comment-counts', 'response_format' => array( 'all' => '(int) Combined number of approved and unapproved comments', 'approved' => '(int) Number of approved comments', 'pending' => '(int) Number of unapproved comments', 'trash' => '(int) Number of trash comments', 'spam' => '(int) Number of spam comments', 'post_trashed' => '(int) Number of comments whose parent post has been trashed', 'total_comments' => '(int) Combined number of comments in each category', ), ) ); /** * GET Comment Counts endpoint class. */ class WPCOM_JSON_API_GET_Comment_Counts_Endpoint extends WPCOM_JSON_API_Endpoint { /** * * API callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. */ public function callback( $path = '', $blog_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( ! get_current_user_id() ) { return new WP_Error( 'authorization_required', 'An active access token must be used to retrieve comment counts.', 403 ); } if ( ! current_user_can_for_blog( $blog_id, 'edit_posts' ) ) { return new WP_Error( 'authorization_required', 'You are not authorized to view comment counts for this blog.', 403 ); } $args = $this->query_args(); // If 0 is passed wp_count_comments will default to fetching counts for the whole site. $post_id = ! empty( $args['post_id'] ) ? (int) $args['post_id'] : 0; // Check if post with given id exists. if ( ! empty( $post_id ) && ! is_object( get_post( $post_id ) ) ) { return new WP_Error( 'invalid_input', 'Provided post_id does not exist', 400 ); } $comment_counts = get_object_vars( $this->api->wp_count_comments( $post_id ) ); // Keys coming from wp_count_comments don't match the ones that we use in // wp-admin and Calypso and are not consistent. Let's normalize the response. return array( 'all' => (int) $comment_counts['all'], 'approved' => (int) $comment_counts['approved'], 'pending' => (int) $comment_counts['moderated'], 'trash' => (int) $comment_counts['trash'], 'spam' => (int) $comment_counts['spam'], 'post_trashed' => (int) $comment_counts['post-trashed'], 'total_comments' => (int) $comment_counts['total_comments'], ); } } class.wpcom-json-api-sharing-buttons-endpoint.php 0000644 00000064133 14722054026 0016200 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound /** * Sharing button endpoint class. */ abstract class WPCOM_JSON_API_Sharing_Button_Endpoint extends WPCOM_JSON_API_Endpoint { /** * All visibilties. * * @var array */ public static $all_visibilities = array( 'visible', 'hidden' ); /** * Sharing service. * * @var Sharing_Service */ protected $sharing_service; /** * Setup function. * * @return null|WP_Error */ protected function setup() { if ( class_exists( 'Sharing_Service' ) ) { $this->sharing_service = new Sharing_Service(); } if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'forbidden', 'You do not have the capability to manage sharing buttons for this site', 403 ); } elseif ( ! class_exists( 'Sharing_Service' ) || ! class_exists( 'Sharing_Source' ) || ( method_exists( 'Jetpack', 'is_module_active' ) && ! Jetpack::is_module_active( 'sharedaddy' ) ) ) { return new WP_Error( 'missing_jetpack_module', 'The Sharing module must be activated in order to use this endpoint', 400 ); } } /** * Format the sharing button. * * @param object $button - the button object. * @return array */ public function format_sharing_button( $button ) { $response = array( 'ID' => $button->get_id(), 'name' => $button->get_name(), 'shortname' => $button->shortname, 'custom' => is_a( $button, 'Share_Custom' ), 'enabled' => $this->is_button_enabled( $button ), ); if ( $response['enabled'] ) { // Status is either "disabled" or the visibility value $response['visibility'] = $this->get_button_visibility( $button ); } if ( ! empty( $button->icon ) ) { // Only pre-defined sharing buttons include genericon $response['genericon'] = $button->icon; } if ( method_exists( $button, 'get_options' ) ) { // merge get_options() values into response, primarily to account // for custom sharing button values foreach ( $button->get_options() as $key => $value ) { // Capitalize URL property if ( 'url' === strtolower( $key ) ) { $key = strtoupper( $key ); } $response[ $key ] = $value; } } return $response; } /** * Get the button visibility. * * @param object $button - the button object. * * @return string|false */ public function get_button_visibility( $button ) { $services = $this->sharing_service->get_blog_services(); $visibilities = self::$all_visibilities; $button_id = $button->get_id(); foreach ( $visibilities as $visibility ) { if ( isset( $services[ $visibility ][ $button_id ] ) ) { return $visibility; } } return false; } /** * Check if the button is enabled. * * @param object $button - the button object. * * @return bool */ public function is_button_enabled( $button ) { return false !== $this->get_button_visibility( $button ); } /** * Check if button is for custom (?). * * @param array $button - the button array. * * @return bool */ protected function is_button_input_for_custom( $button ) { return ( isset( $button['custom'] ) && $button['custom'] ) || ( isset( $button['ID'] ) && 1 === preg_match( '/^custom-/', $button['ID'] ) ) || ! empty( $button['name'] ) || ! empty( $button['URL'] ) || ! empty( $button['icon'] ); } /** * Validate the button input. * * @param array $button - the button array. * @param bool $is_new - if the button is new. * * @return null|WP_Error */ protected function validate_button_input( $button, $is_new = false ) { if ( ! empty( $button['visibility'] ) && ! in_array( $button['visibility'], self::$all_visibilities, true ) ) { return new WP_Error( 'invalid_visibility', sprintf( 'The visibility field must be one of the following values: %s', implode( ', ', self::$all_visibilities ) ), 400 ); } elseif ( $is_new && empty( $button['URL'] ) ) { return new WP_Error( 'invalid_request', 'The URL field is required', 400 ); } elseif ( $is_new && empty( $button['icon'] ) ) { return new WP_Error( 'invalid_request', 'The icon field is required', 400 ); } } /** * Create a custom button. * * @param array $button - the button array. * * @return Share_Custom|false */ public function create_custom_button( $button ) { // Default visibility to 'visible' if enabled if ( empty( $button['visibility'] ) && true === $button['enabled'] ) { $button['visibility'] = 'visible'; } $updated_service = $this->sharing_service->new_service( $button['name'], $button['URL'], $button['icon'] ); if ( false !== $updated_service && ( true === $button['enabled'] || ! empty( $button['visibility'] ) ) ) { $blog_services = $this->sharing_service->get_blog_services(); $blog_services[ $button['visibility'] ][ (string) $updated_service->get_id() ] = $updated_service; $this->sharing_service->set_blog_services( array_keys( $blog_services['visible'] ), array_keys( $blog_services['hidden'] ) ); } return $updated_service; } /** * Update the button. * * @param int $button_id - the button id. * @param array $button - the button array. * * @return Share_Custom|WP_Error */ public function update_button( $button_id, $button ) { $blog_services = $this->sharing_service->get_blog_services(); // Find existing button $all_buttons = $this->sharing_service->get_all_services_blog(); if ( ! array_key_exists( $button_id, $all_buttons ) ) { // Button doesn't exist return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 ); } $updated_service = $all_buttons[ $button_id ]; $service_id = $updated_service->get_id(); if ( is_a( $all_buttons[ $button_id ], 'Share_Custom' ) ) { // Replace options for existing custom button $options = $updated_service->get_options(); $name = isset( $button['name'] ) ? $button['name'] : $options['name']; $url = isset( $button['URL'] ) ? $button['URL'] : $options['url']; $icon = isset( $button['icon'] ) ? $button['icon'] : $options['icon']; $updated_service = new Share_Custom( $service_id, array( 'name' => $name, 'url' => $url, 'icon' => $icon, ) ); $this->sharing_service->set_service( $button_id, $updated_service ); } // Default visibility to 'visible' if enabled if ( empty( $button['visibility'] ) && true === $button['enabled'] ) { $button['visibility'] = 'visible'; } elseif ( false === $button['enabled'] ) { unset( $button['visibility'] ); } // Update button visibility and enabled status $visibility_changed = ( isset( $button['visibility'] ) || true === $button['enabled'] ) && ! array_key_exists( $service_id, $blog_services[ $button['visibility'] ] ); $is_disabling = false === $button['enabled']; if ( $visibility_changed || $is_disabling ) { // Remove from all other visibilities foreach ( $blog_services as $service_visibility => $services ) { if ( $is_disabling || $service_visibility !== $button['visibility'] ) { unset( $blog_services[ $service_visibility ][ $service_id ] ); } } if ( $visibility_changed ) { $blog_services[ $button['visibility'] ][ $service_id ] = $updated_service; } $this->sharing_service->set_blog_services( array_keys( $blog_services['visible'] ), array_keys( $blog_services['hidden'] ) ); } return $updated_service; } } new WPCOM_JSON_API_Get_Sharing_Buttons_Endpoint( array( 'description' => 'Get a list of a site\'s sharing buttons.', 'group' => 'sharing', 'stat' => 'sharing-buttons', 'method' => 'GET', 'path' => '/sites/%s/sharing-buttons/', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'query_parameters' => array( 'enabled_only' => '(bool) If true, only enabled sharing buttons are included in the response', 'visibility' => '(string) The type of enabled sharing buttons to filter by, either "visible" or "hidden"', ), 'response_format' => array( 'found' => '(int) The total number of sharing buttons found that match the request.', 'sharing_buttons' => '(array:object) Array of sharing button objects', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), 'example_response' => ' { "found": 2, "sharing_buttons": [ { "ID": "twitter", "name": "Twitter", "shortname": "twitter", "custom": false, "enabled": true, "visibility": "visible", "genericon": "\\f202" }, { "ID": "facebook", "name": "Facebook", "shortname": "facebook", "custom": false, "enabled": true, "visibility": "visible", "genericon": "\\f203" } ] }', ) ); /** * Get sharing buttons endpoint class. * * GET /sites/%s/sharing-buttons -> $blog_id */ class WPCOM_JSON_API_Get_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { $args = $this->query_args(); // Validate request $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $continue = $this->setup(); if ( is_wp_error( $continue ) ) { return $continue; } if ( ! empty( $args['visibility'] ) && ! in_array( $args['visibility'], self::$all_visibilities, true ) ) { return new WP_Error( 'invalid_visibility', sprintf( 'The visibility field must be one of the following values: %s', implode( ', ', self::$all_visibilities ) ), 400 ); } // Determine which visibilities to include based on request $visibilities = empty( $args['visibility'] ) ? self::$all_visibilities : array( $args['visibility'] ); // Discover enabled services $buttons = array(); $enabled_services = $this->sharing_service->get_blog_services(); $all_services = $this->sharing_service->get_all_services_blog(); // Include buttons of desired visibility foreach ( $visibilities as $visibility ) { $buttons = array_merge( $buttons, $enabled_services[ $visibility ] ); } // Unless `enabled_only` or `visibility` is specified, append the // remaining buttons to the end of the array if ( ( ! isset( $args['enabled_only'] ) || ! $args['enabled_only'] ) && empty( $args['visibility'] ) ) { foreach ( $all_services as $id => $button ) { if ( ! array_key_exists( $id, $buttons ) ) { $buttons[ $id ] = $button; } } } // Format each button in the response $response = array(); foreach ( $buttons as $button ) { $response[] = $this->format_sharing_button( $button ); } return array( 'found' => count( $response ), 'sharing_buttons' => $response, ); } } new WPCOM_JSON_API_Get_Sharing_Button_Endpoint( array( 'description' => 'Get information about a single sharing button.', 'group' => '__do_not_document', 'stat' => 'sharing-buttons:1', 'method' => 'GET', 'path' => '/sites/%s/sharing-buttons/%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$button_id' => '(string) The button ID', ), 'response_format' => array( 'ID' => '(int) Sharing button ID', 'name' => '(string) Sharing button name, used as a label on the button itself', 'shortname' => '(string) A generated short name for the sharing button', 'URL' => '(string) The URL pattern defined for a custom sharing button', 'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button', 'genericon' => '(string) Icon character in Genericons icon set', 'custom' => '(bool) Is the button a user-created custom sharing button?', 'enabled' => '(bool) Is the button currently enabled for the site?', 'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/facebook', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), 'example_response' => '{ "ID": "facebook", "name": "Facebook", "shortname": "facebook", "custom": false, "enabled": true, "visibility": "visible", "genericon": "\\f203" }', ) ); /** * Get sharing button endpoint class. * * GET /sites/%s/sharing-buttons/%s -> $blog_id, $button_id */ class WPCOM_JSON_API_Get_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $button_id - the button id. * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0, $button_id = 0 ) { // Validate request $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $continue = $this->setup(); if ( is_wp_error( $continue ) ) { return $continue; } // Search existing services for button $all_buttons = $this->sharing_service->get_all_services_blog(); if ( ! array_key_exists( $button_id, $all_buttons ) ) { return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 ); } else { return $this->format_sharing_button( $all_buttons[ $button_id ] ); } } } new WPCOM_JSON_API_Update_Sharing_Buttons_Endpoint( array( 'description' => 'Edit all sharing buttons for a site.', 'group' => 'sharing', 'stat' => 'sharing-buttons:X:POST', 'method' => 'POST', 'path' => '/sites/%s/sharing-buttons', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'sharing_buttons' => '(array:sharing_button) An array of sharing button objects', ), 'response_format' => array( 'success' => '(bool) Confirmation that all sharing buttons were updated as specified', 'updated' => '(array) An array of updated sharing buttons', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'sharing_buttons' => array( array( 'ID' => 'facebook', 'visibility' => 'hidden', ), ), ), ), 'example_response' => '{ "success": true, "updated": [ { "ID": "facebook", "name": "Facebook", "shortname": "facebook", "custom": false, "enabled": true, "visibility": "hidden", "genericon": "\\f204" } ] }', ) ); /** * Update sharing buttons endpoint. * * POST /sites/%s/sharing-buttons -> $blog_id */ class WPCOM_JSON_API_Update_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0 ) { $input = $this->input(); // Validate request $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $continue = $this->setup(); if ( is_wp_error( $continue ) ) { return $continue; } $all_buttons = $this->sharing_service->get_all_services_blog(); if ( ! isset( $input['sharing_buttons'] ) ) { $input['sharing_buttons'] = array(); } // We do a first pass of all buttons to verify that no validation // issues exist before continuing to update foreach ( $input['sharing_buttons'] as $button ) { $button_exists = isset( $button['ID'] ) && array_key_exists( $button['ID'], $all_buttons ); $is_custom = $this->is_button_input_for_custom( $button ); // If neither custom nor existing, bail if ( ! $button_exists && ! $is_custom ) { return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 ); } // Validate input, only testing custom values if the button doesn't // already exist $validation_error = $this->validate_button_input( $button, ! $button_exists ); if ( is_wp_error( $validation_error ) ) { return $validation_error; } } // Reset all existing buttons $this->sharing_service->set_blog_services( array(), array() ); // Finally, we iterate over each button and update or create $success = true; $updated = array(); foreach ( $input['sharing_buttons'] as $button ) { $button_exists = isset( $button['ID'] ) && array_key_exists( $button['ID'], $all_buttons ); if ( $button_exists ) { $updated_service = $this->update_button( $button['ID'], $button ); } else { $updated_service = $this->create_custom_button( $button ); } // We'll allow the request to continue if a failure occurred, but // log it for the response if ( false === $updated_service ) { $success = false; } else { $updated[] = $this->format_sharing_button( $updated_service ); } } return array( 'success' => $success, 'updated' => $updated, ); } } new WPCOM_JSON_API_Update_Sharing_Button_Endpoint( array( 'description' => 'Create a new custom sharing button.', 'group' => '__do_not_document', 'stat' => 'sharing-buttons:new', 'method' => 'POST', 'path' => '/sites/%s/sharing-buttons/new', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', ), 'request_format' => array( 'name' => '(string) The name for your custom sharing button, used as a label on the button itself', 'URL' => '(string) The URL to use for share links, including optional placeholders (%post_id%, %post_title%, %post_slug%, %post_url%, %post_full_url%, %post_excerpt%, %post_tags%, %home_url%)', 'icon' => '(string) The full URL to a 16x16 icon to display on the sharing button', 'enabled' => '(bool) Is the button currently enabled for the site?', 'visibility' => '(string) If enabled, the visibility of the sharing button, either "visible" (default) or "hidden"', ), 'response_format' => array( 'ID' => '(string) Sharing button ID', 'name' => '(string) Sharing button name, used as a label on the button itself', 'shortname' => '(string) A generated short name for the sharing button', 'URL' => '(string) The URL pattern defined for a custom sharing button', 'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button', 'genericon' => '(string) Icon character in Genericons icon set', 'custom' => '(bool) Is the button a user-created custom sharing button?', 'enabled' => '(bool) Is the button currently enabled for the site?', 'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/new/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'name' => 'Custom', 'URL' => 'https://www.wordpress.com/%post_name%', 'icon' => 'https://en.wordpress.com/i/stats-icon.gif', 'enabled' => true, 'visibility' => 'visible', ), ), 'example_response' => '{ "ID": "custom-123456789", "name": "Custom", "shortname": "custom", "url": "https://www.wordpress.com/%post_name%", "icon": "https://en.wordpress.com/i/stats-icon.gif", "custom": true, "enabled": true, "visibility": "visible" }', ) ); new WPCOM_JSON_API_Update_Sharing_Button_Endpoint( array( 'description' => 'Edit a sharing button.', 'group' => '__do_not_document', 'stat' => 'sharing-buttons:1:POST', 'method' => 'POST', 'path' => '/sites/%s/sharing-buttons/%s', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$button_id' => '(string) The button ID', ), 'request_format' => array( 'name' => '(string) Only if a custom sharing button, a new name used as a label on the button itself', 'URL' => '(string) Only if a custom sharing button, the URL to use for share links, including optional placeholders (%post_title%, %post_url%, %post_full_url%, %post_excerpt%, %post_tags%)', 'icon' => '(string) Only if a custom sharing button, the full URL to a 16x16 icon to display on the sharing button', 'enabled' => '(bool) Is the button currently enabled for the site?', 'visibility' => '(string) If enabled, the visibility of the sharing button, either "visible" (default) or "hidden"', ), 'response_format' => array( 'ID' => '(string) Sharing button ID', 'name' => '(string) Sharing button name, used as a label on the button itself', 'shortname' => '(string) A generated short name for the sharing button', 'URL' => '(string) The URL pattern defined for a custom sharing button', 'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button', 'genericon' => '(string) Icon character in Genericons icon set', 'custom' => '(bool) Is the button a user-created custom sharing button?', 'enabled' => '(bool) Is the button currently enabled for the site?', 'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/custom-123456789/', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), 'body' => array( 'enabled' => false, ), ), 'example_response' => '{ "ID": "custom-123456789", "name": "Custom", "shortname": "custom", "custom": true, "enabled": false, "icon": "https://en.wordpress.com/i/stats-icon.gif", "url": "https://www.wordpress.com/%post_name%" }', ) ); /** * Sharing button endpoint class. * * POST /sites/%s/sharing-buttons/new -> $blog_id * POST /sites/%s/sharing-buttons/%s -> $blog_id, $button_id */ class WPCOM_JSON_API_Update_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $button_id - the button ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0, $button_id = 0 ) { $new = $this->api->ends_with( $path, '/new' ); $input = $this->input(); // Validate request $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $continue = $this->setup(); if ( is_wp_error( $continue ) ) { return $continue; } $validation_error = $this->validate_button_input( $input, $new ); if ( is_wp_error( $validation_error ) ) { return $validation_error; } // Update or create button if ( $new ) { $updated_service = $this->create_custom_button( $input ); } else { $updated_service = $this->update_button( $button_id, $input ); } if ( false === $updated_service ) { return new WP_Error( 'invalid_request', sprintf( 'The sharing button was not %s', $new ? 'created' : 'updated' ), 400 ); } elseif ( is_wp_error( $updated_service ) ) { return $updated_service; } else { return $this->format_sharing_button( $updated_service ); } } } new WPCOM_JSON_API_Delete_Sharing_Button_Endpoint( array( 'description' => 'Delete a custom sharing button.', 'group' => '__do_not_document', 'stat' => 'sharing-buttons:1:delete', 'method' => 'POST', 'path' => '/sites/%s/sharing-buttons/%s/delete', 'path_labels' => array( '$site' => '(int|string) Site ID or domain', '$button_id' => '(string) The button ID', ), 'response_format' => array( 'ID' => '(int) The ID of the deleted sharing button', 'success' => '(bool) Confirmation that the sharing button has been removed', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/custom-123456789/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ), ), 'example_response' => '{ "ID": "custom-123456789", "success": "true" }', ) ); /** * Delete sharing button endpoint class. * * POST /sites/%s/sharing-buttons/%s/delete -> $blog_id, $button_id */ class WPCOM_JSON_API_Delete_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint { /** * API Callback. * * @param string $path - the path. * @param int $blog_id - the blog ID. * @param int $button_id - the button ID. * * @return array|WP_Error */ public function callback( $path = '', $blog_id = 0, $button_id = 0 ) { // Validate request $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } $continue = $this->setup(); if ( is_wp_error( $continue ) ) { return $continue; } // Find existing button $all_buttons = $this->sharing_service->get_all_services_blog(); if ( ! array_key_exists( $button_id, $all_buttons ) ) { // Button doesn't exist return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 ); } // Verify button is custom if ( ! is_a( $all_buttons[ $button_id ], 'Share_Custom' ) ) { return new WP_Error( 'invalid_request', 'Only custom sharing buttons can be deleted', 400 ); } $success = $this->sharing_service->delete_service( $button_id ); return array( 'ID' => $button_id, 'success' => $success, ); } } class.wpcom-json-api-update-user-endpoint.php 0000644 00000013364 14722054026 0015307 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Update site users API endpoint. * * Endpoint: /sites/%s/users/%d/delete */ new WPCOM_JSON_API_Update_User_Endpoint( array( 'description' => 'Deletes or removes a user of a site.', 'group' => 'users', 'stat' => 'users:delete', 'method' => 'POST', 'path' => '/sites/%s/users/%d/delete', 'path_labels' => array( '$site' => '(int|string) The site ID or domain.', '$user_ID' => '(int) The user\'s ID', ), 'request_format' => array( 'reassign' => '(int) An optional id of a user to reassign posts to.', ), 'response_format' => array( 'success' => '(bool) Was the deletion of user successful?', ), 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/users/1/delete', 'example_request_data' => array( 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN', ), ), 'example_response' => ' { "success": true }', ) ); /** * Update site users API class. */ class WPCOM_JSON_API_Update_User_Endpoint extends WPCOM_JSON_API_Endpoint { /** * Update site users API callback. * * @param string $path API path. * @param int $blog_id Blog ID. * @param int $user_id User ID. */ public function callback( $path = '', $blog_id = 0, $user_id = 0 ) { $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); if ( is_wp_error( $blog_id ) ) { return $blog_id; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { if ( (int) wpcom_get_blog_owner( $blog_id ) === (int) $user_id ) { return new WP_Error( 'forbidden', 'A site owner can not be removed through this endpoint.', 403 ); } } if ( $this->api->ends_with( $path, '/delete' ) ) { return $this->delete_or_remove_user( $user_id ); } return false; } /** * Checks if a user exists by checking to see if a WP_User object exists for a user ID. * * @param int $user_id User ID. * @return bool */ public function user_exists( $user_id ) { $user = get_user_by( 'id', $user_id ); return false !== $user && is_a( $user, 'WP_User' ); } /** * Return the domain name of a subscription. * * @param Store_Subscription $subscription Subscription object. * @return string */ protected function get_subscription_domain_name( $subscription ) { return $subscription->meta; } /** * Get a list of the domains owned by the given user. * * @param int $user_id User ID. * @return array */ protected function domain_subscriptions_for_site_owned_by_user( $user_id ) { $subscriptions = WPCOM_Store::get_subscriptions( get_current_blog_id(), $user_id, domains::get_domain_products() ); $domains = array_unique( array_map( array( $this, 'get_subscription_domain_name' ), $subscriptions ) ); return array_values( $domains ); } /** * Validates user input and then decides whether to remove or delete a user. * * @param int $user_id User ID. * @return array|WP_Error */ public function delete_or_remove_user( $user_id ) { if ( 0 === (int) $user_id ) { return new WP_Error( 'invalid_input', 'A valid user ID must be specified.', 400 ); } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $domains = $this->domain_subscriptions_for_site_owned_by_user( $user_id ); if ( ! empty( $domains ) ) { $error = new WP_Error( 'user_owns_domain_subscription', implode( ', ', $domains ) ); $error->add_data( $domains, 'additional_data' ); return $error; } $active_user_subscriptions = WPCOM_Store::get_user_subscriptions( $user_id, get_current_blog_id() ); if ( ! empty( $active_user_subscriptions ) ) { $product_names = array_values( wp_list_pluck( $active_user_subscriptions, 'product_name' ) ); $error = new WP_Error( 'user_has_active_subscriptions', 'User has active subscriptions' ); $error->add_data( $product_names, 'additional_data' ); return $error; } } if ( get_current_user_id() === (int) $user_id ) { return new WP_Error( 'invalid_input', 'User can not remove or delete self through this endpoint.', 400 ); } if ( ! $this->user_exists( $user_id ) ) { return new WP_Error( 'invalid_input', 'A user does not exist with that ID.', 400 ); } return is_multisite() ? $this->remove_user( $user_id ) : $this->delete_user( $user_id ); } /** * Removes a user from the current site. * * @param int $user_id User ID. * @return array|WP_Error */ public function remove_user( $user_id ) { if ( ! current_user_can( 'remove_users' ) ) { return new WP_Error( 'unauthorized', 'User cannot remove users for specified site.', 403 ); } if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { return new WP_Error( 'invalid_input', 'User is not a member of the specified site.', 400 ); } return array( 'success' => remove_user_from_blog( $user_id, get_current_blog_id() ), ); } /** * Deletes a user and optionally reassigns posts to another user. * * @param int $user_id User ID. * @return array|WP_Error */ public function delete_user( $user_id ) { if ( ! current_user_can( 'delete_users' ) ) { return new WP_Error( 'unauthorized', 'User cannot delete users for specified site.', 403 ); } $input = (array) $this->input(); if ( isset( $input['reassign'] ) ) { if ( (int) $user_id === (int) $input['reassign'] ) { return new WP_Error( 'invalid_input', 'Can not reassign posts to user being deleted.', 400 ); } if ( ! $this->user_exists( $input['reassign'] ) ) { return new WP_Error( 'invalid_input', 'User specified in reassign argument is not a member of the specified site.', 400 ); } } return array( 'success' => wp_delete_user( $user_id, (int) $input['reassign'] ), ); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.3-4ubuntu2.24 | Генерация страницы: 0.21 |
proxy
|
phpinfo
|
Настройка