summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/json-endpoints')
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-add-widget-endpoint.php103
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-autosave-post-v1-1-endpoint.php112
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-delete-post-endpoint.php68
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-restore-post-endpoint.php69
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-update-comments-endpoint.php219
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-comment-endpoint.php207
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-endpoint.php58
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-v1-1-endpoint.php69
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-edit-media-v1-2-endpoint.php428
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-autosave-v1-1-endpoint.php72
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-counts-endpoint.php72
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-endpoint.php38
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-history-endpoint.php53
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-endpoint.php187
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php92
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php148
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-customcss.php58
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-endpoint.php49
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-1-endpoint.php64
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-2-endpoint.php72
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-counts-v1-1-endpoint.php137
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-endpoint.php81
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-v1-1-endpoint.php73
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-endpoint.php749
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-v1-2-endpoint.php62
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomies-endpoint.php144
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomy-endpoint.php59
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-get-term-endpoint.php54
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-comments-endpoint.php309
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-embeds-endpoint.php60
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-endpoint.php82
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-1-endpoint.php288
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-2-endpoint.php77
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-type-taxonomies-endpoint.php98
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php117
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-endpoint.php353
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-1-endpoint.php529
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-2-endpoint.php433
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-roles-endpoint.php135
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-shortcodes-endpoint.php48
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-terms-endpoint.php112
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php177
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php824
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-post-endpoint.php658
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-post-v1-1-endpoint.php353
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-endpoint.php75
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-reversal-endpoint.php108
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-render-endpoint.php145
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-render-shortcode-endpoint.php71
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-sharing-buttons-endpoint.php644
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php863
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-2-endpoint.php164
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-3-endpoint.php157
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-4-endpoint.php127
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-site-user-endpoint.php221
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-taxonomy-endpoint.php30
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-comment-endpoint.php393
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-customcss.php86
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-endpoint.php80
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-v1-1-endpoint.php134
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-endpoint.php921
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-1-endpoint.php1005
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-2-endpoint.php873
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-homepage-endpoint.php77
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-logo-endpoint.php96
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-taxonomy-endpoint.php314
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-term-endpoint.php238
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-update-user-endpoint.php165
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-endpoint.php94
-rw-r--r--plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-v1-1-endpoint.php176
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-check-capabilities-endpoint.php26
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-endpoint.php20
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-modify-endpoint.php75
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php252
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-endpoint.php128
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-comment-backup-endpoint.php52
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-database-object-backup-endpoint.php97
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-option-backup-endpoint.php35
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-post-backup-endpoint.php31
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-term-backup-endpoint.php32
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-user-backup-endpoint.php32
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-jps-woocommerce-connect-endpoint.php58
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-log-endpoint.php16
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-maybe-auto-update-endpoint.php32
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-endpoint.php125
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-get-endpoint.php6
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-list-endpoint.php13
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-modify-endpoint.php62
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-delete-endpoint.php78
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-endpoint.php321
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-get-endpoint.php28
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-install-endpoint.php96
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-list-endpoint.php36
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-endpoint.php420
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-v1-2-endpoint.php191
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-new-endpoint.php136
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-sync-endpoint.php322
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-active-endpoint.php50
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-delete-endpoint.php60
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-endpoint.php178
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-get-endpoint.php6
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-install-endpoint.php173
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-list-endpoint.php13
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-modify-endpoint.php130
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-new-endpoint.php83
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-endpoint.php20
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-modify-endpoint.php29
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-updates-status-endpoint.php34
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-connect-endpoint.php30
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-create-endpoint.php72
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php41
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-update-option-endpoint.php31
-rw-r--r--plugins/jetpack/json-endpoints/jetpack/json-api-jetpack-endpoints.php1234
113 files changed, 20411 insertions, 0 deletions
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-add-widget-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-add-widget-endpoint.php
new file mode 100644
index 00000000..2c1a7eeb
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-add-widget-endpoint.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Activate a widget on a site.
+ *
+ * https://public-api.wordpress.com/rest/v1.1/sites/$site/widgets/new
+ */
+
+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
+ }'
+) );
+
+
+class WPCOM_JSON_API_Add_Widgets_Endpoint extends WPCOM_JSON_API_Endpoint {
+ /**
+ * API callback.
+ *
+ * @param string $path
+ * @param int $blog_id
+ * @uses jetpack_require_lib
+ * @uses Jetpack_Widgets
+ *
+ * @return array|WP_Error
+ */
+ 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 );
+ }
+
+ jetpack_require_lib( 'widgets' );
+ $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'] );
+ }
+
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-autosave-post-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-autosave-post-v1-1-endpoint.php
new file mode 100644
index 00000000..0acb66f0
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-autosave-post-v1-1-endpoint.php
@@ -0,0 +1,112 @@
+<?php
+
+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',
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
+ function __construct( $args ) {
+ parent::__construct( $args );
+ }
+
+ // /sites/%s/posts/%d/autosave -> $blog_id, $post_id
+ 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();
+
+ $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() == $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' => $auto_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 ),
+ 'preview_URL' => $preview_url
+ );
+ } else {
+ return new WP_Error( 'autosave_error', __( 'Autosave encountered an unexpected error', 'jetpack' ), 500 );
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-delete-post-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-delete-post-endpoint.php
new file mode 100644
index 00000000..31ab8d95
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-delete-post-endpoint.php
@@ -0,0 +1,68 @@
+<?php
+
+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 ),
+ ),
+
+ )
+) );
+
+class WPCOM_JSON_API_Bulk_Delete_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint {
+ // /sites/%s/posts/delete
+ 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'];
+ } else if ( ! 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-restore-post-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-restore-post-endpoint.php
new file mode 100644
index 00000000..2e59cc1e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-restore-post-endpoint.php
@@ -0,0 +1,69 @@
+<?php
+
+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 ),
+ ),
+
+ )
+) );
+
+class WPCOM_JSON_API_Bulk_Restore_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint {
+ // /sites/%s/posts/restore
+ // The unused $object parameter is for making the method signature compatible with its parent class method.
+ function callback( $path = '', $blog_id = 0, $object = null ) {
+ $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'];
+ } else if ( ! 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-update-comments-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-update-comments-endpoint.php
new file mode 100644
index 00000000..3468b81e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-bulk-update-comments-endpoint.php
@@ -0,0 +1,219 @@
+<?php
+
+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 ),
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Bulk_Update_Comments_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/comments/status
+ // /sites/%s/comments/delete
+ 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'];
+ } else if ( 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
+ *
+ * @return boolean
+ */
+ 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
+ *
+ * @return boolean
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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 ( count( $comment_ids ) < 1 ) {
+ return array();
+ }
+
+ return $this->bulk_delete_comments( $comment_ids );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-comment-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-comment-endpoint.php
new file mode 100644
index 00000000..20aee2b8
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-comment-endpoint.php
@@ -0,0 +1,207 @@
+<?php
+
+
+abstract class WPCOM_JSON_API_Comment_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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.',
+ );
+
+ // public $response_format =& $this->comment_object_format;
+
+ function __construct( $args ) {
+ if ( !$this->response_format ) {
+ $this->response_format =& $this->comment_object_format;
+ }
+ parent::__construct( $args );
+ }
+
+ 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' );
+ 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;
+ 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' : // (object|false)
+ if ( $comment->comment_parent ) {
+ $parent = get_comment( $comment->comment_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, $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;
+ }
+ }
+
+ unset( $GLOBALS['comment'], $GLOBALS['post'] );
+ return $response;
+ }
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-endpoint.php
new file mode 100644
index 00000000..c32be52f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-endpoint.php
@@ -0,0 +1,58 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Delete_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-v1-1-endpoint.php
new file mode 100644
index 00000000..cf402f0d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-delete-media-v1-1-endpoint.php
@@ -0,0 +1,69 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Delete_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-edit-media-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-edit-media-v1-2-endpoint.php
new file mode 100644
index 00000000..89d1a67c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-edit-media-v1-2-endpoint.php
@@ -0,0 +1,428 @@
+<?php
+
+jetpack_require_lib( 'class.media' );
+
+define( 'REVISION_HISTORY_MAXIMUM_AMOUNT', 0 );
+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' => "(object) 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/posts/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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Edit_Media_v1_2_Endpoint extends WPCOM_JSON_API_Update_Media_v1_1_Endpoint {
+ /**
+ * Return an array of mime_type items allowed when the media file is uploaded.
+ *
+ * @return {Array} mime_type array
+ */
+ 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 {Number} $media_id - post media ID
+ * @param {Object} $attrs - `attrs` parameter sent from the client in the request body
+ * @return
+ */
+ private function update_by_attrs_parameter( $media_id, $attrs ) {
+ $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 ( 0 === strpos( $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
+ * @return {Boolean} `true` if the file has been removed.
+ * `false` either the file doesn't exist or it couldn't be removed.
+ */
+ private function remove_tmp_file( $file_array ) {
+ if ( ! file_exists ( $file_array['tmp_name'] ) ) {
+ return false;
+ }
+ return @unlink( $file_array['tmp_name'] );
+ }
+
+ /**
+ * Save the given temporal file in a local folder.
+ *
+ * @param {Array} $file_array
+ * @param {Number} $media_id
+ * @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 ) {
+ $tmp_filename = $file_array['tmp_name'];
+
+ if ( ! file_exists( $tmp_filename ) ) {
+ 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 )
+ ) {
+ @unlink( $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;
+ $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 $media_id
+ *
+ * @return string
+ */
+ private function get_time_string_from_guid( $media_id ) {
+ $time = date( "Y/m", strtotime( current_time( 'mysql' ) ) );
+ if ( $media = get_post( $media_id ) ) {
+ $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 {Number} $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 = @parse_url( $url );
+ if ( empty( $parsed ) ) {
+ return new WP_Error( 'invalid_url', 'No media provided in url.' );
+ }
+
+ // save the remote image into a tmp file
+ $tmp = download_url( wpcom_get_private_file( $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 {file} $file - file recentrly added
+ * @param {Boolean} $has_original_media - condition is the original media has been already added
+ * @return {Boolean} `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 ) );
+ }
+
+ 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 = $input['media'] ? (array) $input['media'] : null;
+ $media_url = $input['media_url'];
+ $media_attrs = $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
+ $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;
+ }
+
+ $uploaded_file = $this->save_temporary_file( $temporal_file, $media_id );
+
+ 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;
+ }
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-autosave-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-autosave-v1-1-endpoint.php
new file mode 100644
index 00000000..6b58a08d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-autosave-v1-1-endpoint.php
@@ -0,0 +1,72 @@
+<?php
+
+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',
+) );
+
+class WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
+ function __construct( $args ) {
+ parent::__construct( $args );
+ }
+
+ // /sites/%s/posts/%d/autosave -> $blog_id, $post_id
+ 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' => $auto_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 )
+ );
+ } else {
+ return new WP_Error( 'not_found', 'No autosaves exist for this post', 404 );
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-counts-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-counts-endpoint.php
new file mode 100644
index 00000000..705f2e61
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-counts-endpoint.php
@@ -0,0 +1,72 @@
+<?php
+
+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',
+ )
+) );
+
+class WPCOM_JSON_API_GET_Comment_Counts_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ // /sites/%s/comment-counts
+ 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'] ) ? intval( $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']
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-endpoint.php
new file mode 100644
index 00000000..3a928bfd
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-endpoint.php
@@ -0,0 +1,38 @@
+<?php
+
+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'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/147564'
+) );
+
+class WPCOM_JSON_API_Get_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
+ // /sites/%s/comments/%d -> $blog_id, $comment_id
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-history-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-history-endpoint.php
new file mode 100644
index 00000000..37aa9206
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comment-history-endpoint.php
@@ -0,0 +1,53 @@
+<?php
+
+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.'
+ )
+) );
+
+class WPCOM_JSON_API_GET_Comment_History_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ // /sites/%s/comment-history/%d
+ 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'] = intval( $item['time'] );
+ }
+
+ return array( 'comment_history' => $comment_history );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-endpoint.php
new file mode 100644
index 00000000..fdc3bee7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-endpoint.php
@@ -0,0 +1,187 @@
+<?php
+
+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'
+) );
+
+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
+ */
+ 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.
+ */
+ 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.
+ */
+ 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 );
+ // An empty value in the comments_type column denotes a regular comment.
+ $type = ( '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 intval( $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.
+ */
+ 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
+ *
+ * @return boolean
+ */
+ function validate_status_param( $status ) {
+ return in_array( $status, array( 'all', 'approved', 'unapproved', 'pending', 'spam', 'trash' ) );
+ }
+
+ /**
+ * Sanitize a given comment type.
+ *
+ * @param string Comment type: can be 'trackback', 'pingback', or 'comment'.
+ *
+ * @return string Sanitized comment type.
+ */
+ function get_sanitized_comment_type( $type = 'comment' ) {
+ if ( in_array( $type, array( 'trackback', 'pingback', 'comment' ) ) ) {
+ return $type;
+ }
+ return 'comment';
+ }
+
+ /**
+ * Endpoint callback for /sites/%s/comments-tree
+ *
+ * @param string $path
+ * @param int $blog_id
+ *
+ * @return array Site tree results by status.
+ */
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php
new file mode 100644
index 00000000..82124992
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php
@@ -0,0 +1,92 @@
+<?php
+
+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'
+) );
+
+class WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_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
+ */
+ 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 = intval( $row[0] );
+ $comment_post_id = intval( $row[1] );
+ $comment_parent_id = intval( $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,
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php
new file mode 100644
index 00000000..0537279d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php
@@ -0,0 +1,148 @@
+<?php
+
+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',
+) );
+
+class WPCOM_JSON_API_Get_Comments_Tree_v1_2_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint {
+ /**
+ * 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
+ */
+ 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 = intval( $row[0] );
+ $comment_parent_id = intval( $row[1] );
+ $comment_post_id = isset( $args['post_id'] ) ? intval( $args['post_id'] ) : intval( $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
+ * @param int $blog_id
+ *
+ * @return array Site or post tree results by status.
+ */
+ 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 ( is_null( get_post( absint( $args['post_id'] ) ) ) ) {
+ return new WP_Error( 'invalid_post', 'Invalid post', 400 );
+ }
+ $filters['post_id'] = absint( $args['post_id'] );
+ }
+
+ return $this->get_site_tree_v1_2( $filters );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-customcss.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-customcss.php
new file mode 100644
index 00000000..ecc9f998
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-customcss.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Custom Css 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"
+ }'
+) );
+
+class WPCOM_JSON_API_Get_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint {
+ /**
+ * API callback.
+ */
+ 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 );
+ }
+}
+
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-endpoint.php
new file mode 100644
index 00000000..cee9dac5
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-endpoint.php
@@ -0,0 +1,49 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Get_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-1-endpoint.php
new file mode 100644
index 00000000..62d8681e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-1-endpoint.php
@@ -0,0 +1,64 @@
+<?php
+
+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',
+ '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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Get_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-2-endpoint.php
new file mode 100644
index 00000000..413e3b94
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-media-v1-2-endpoint.php
@@ -0,0 +1,72 @@
+<?php
+
+jetpack_require_lib( 'class.media' );
+
+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',
+ '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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Get_Media_v1_2_Endpoint extends WPCOM_JSON_API_Get_Media_v1_1_Endpoint {
+ 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;
+ }
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-counts-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-counts-v1-1-endpoint.php
new file mode 100644
index 00000000..5e83b41d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-counts-v1-1-endpoint.php
@@ -0,0 +1,137 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_GET_Post_Counts_V1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ private $whitelist = array( 'publish' );
+
+ /**
+ * Build SQL query
+ *
+ * @param {String} type - post type
+ * @param {Number} [author]
+ * @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";
+
+ return $wpdb->prepare( $query, $post_type, $user_id );
+ }
+
+ /**
+ * Retrive counts using wp_cache
+ *
+ * @param {String} $post_type
+ * @param {Number} [$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->whitelist ) && $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 ) {
+ $results = $wpdb->get_results( $this->buildCountsQuery( $post_type, $id ) );
+ $counts = $this->filterStatusesByWhiteslist( $results );
+ wp_cache_set( $key, $counts, 'counts' );
+ }
+
+ return $counts;
+ }
+
+ private function filterStatusesByWhiteslist( $in ) {
+ $return = array();
+ foreach( $in as $result) {
+ if ( in_array( $result->status, $this->whitelist ) ) {
+ $return[ $result->status ] = (int) $result->count;
+ }
+ };
+ return $return;
+ }
+
+ // /sites/%s/post-counts/%s
+ 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;
+ }
+
+ 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();
+
+ if ( current_user_can( 'edit_posts' ) ) {
+ array_push( $this->whitelist, 'draft', 'future', 'pending', 'private', 'trash' );
+ }
+
+ $return = array(
+ 'counts' => (array) array(
+ 'all' => (object) $this->retrieveCounts( $post_type ),
+ 'mine' => (object) $this->retrieveCounts( $post_type, $mine_ID ),
+ )
+ );
+
+ // AUTHOR
+ if ( isset( $args['author'] ) ) {
+ $author_ID = $args['author'];
+ $return['counts']['author'] = (object) $this->retrieveCounts( $post_type, $author_ID );
+ }
+
+ return (object) $return;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-endpoint.php
new file mode 100644
index 00000000..f82b2f43
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-endpoint.php
@@ -0,0 +1,81 @@
+<?php
+
+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',
+ ),
+
+ '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)',
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff',
+) );
+
+class WPCOM_JSON_API_Get_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
+ // /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
+ 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 ( false === strpos( $path, '/posts/slug:' ) && false === strpos( $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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-v1-1-endpoint.php
new file mode 100644
index 00000000..572567d7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-post-v1-1-endpoint.php
@@ -0,0 +1,73 @@
+<?php
+
+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',
+ ),
+ '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)',
+ ),
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff',
+) );
+
+class WPCOM_JSON_API_Get_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
+ // /sites/%s/posts/%d -> $blog_id, $post_id
+ // /sites/%s/posts/slug:%s -> $blog_id, $post_id
+ 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();
+
+ $site = $this->get_platform()->get_site( $blog_id );
+
+ if ( false !== strpos( $path, '/posts/slug:' ) ) {
+ $post_id = $site->get_post_id_by_name( $post_id );
+ if ( is_wp_error( $post_id ) ) {
+ return $post_id;
+ }
+ }
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM &&
+ ! in_array( get_post_type( $post_id ), array( false, 'post', 'revision' ) ) ) {
+ $this->load_theme_functions();
+ }
+
+ $return = $this->get_post_by( 'ID', $post_id, $args['context'] );
+
+ if ( !$return || is_wp_error( $return ) ) {
+ return $return;
+ }
+
+ if ( ! $site->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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-endpoint.php
new file mode 100644
index 00000000..f7ab0795
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-endpoint.php
@@ -0,0 +1,749 @@
+<?php
+
+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,
+ '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/',
+) );
+
+class WPCOM_JSON_API_GET_Site_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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',
+ 'is_multisite' => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.',
+ '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',
+ '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',
+ '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.',
+ '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',
+ );
+
+ protected static $no_member_fields = array(
+ 'ID',
+ 'name',
+ 'description',
+ 'URL',
+ 'jetpack',
+ 'post_count',
+ 'subscribers_count',
+ 'lang',
+ 'locale',
+ 'icon',
+ 'logo',
+ 'visible',
+ 'is_private',
+ 'is_following',
+ 'meta',
+ 'launch_status',
+ );
+
+ 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',
+ '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',
+ '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',
+ 'design_type',
+ 'site_goals',
+ 'site_segment',
+ );
+
+ protected static $jetpack_response_field_additions = array(
+ 'subscribers_count',
+ );
+
+ protected static $jetpack_response_field_member_additions = array(
+ 'capabilities',
+ 'plan',
+ );
+
+ 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',
+ 'frame_nonce',
+ 'jetpack_frame_nonce',
+ 'design_type',
+ 'wordads',
+ );
+
+ private $site;
+
+ // protected $compact = null;
+ protected $fields_to_include = '_all';
+ protected $options_to_include = '_all';
+
+ // /sites/mine
+ // /sites/%s -> $blog_id
+ 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'];
+ }
+
+ $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;
+ }
+
+ 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;
+
+ if ( ! $this->has_blog_access( $this->api->token_details, $blog_id ) ) {
+ $response_keys = array_intersect( $response_keys, self::$no_member_fields );
+ }
+
+ return $this->render_response_keys( $response_keys );
+ }
+
+ /**
+ * Checks that the current user has access to the current blog,
+ * and failing that checks that we have a valid blog token.
+ *
+ * @param $token_details array Details obtained from the authorization token
+ * @param $blog_id int The server-side blog id on wordpress.com
+ *
+ * @return bool
+ */
+ private function has_blog_access( $token_details, $blog_id ) {
+ $current_blog_id = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ?
+ $blog_id :
+ get_current_blog_id();
+
+ if ( is_user_member_of_blog( get_current_user_id(), $current_blog_id ) ) {
+ return true;
+ }
+
+ $token_details = (array) $token_details;
+ if ( ! isset( $token_details['access'], $token_details['auth'], $token_details['blog_id'] ) ) {
+ return false;
+ }
+
+ if (
+ 'jetpack' === $token_details['auth'] &&
+ 'blog' === $token_details['access'] &&
+ $current_blog_id === $token_details['blog_id']
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ 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;
+ }
+
+ 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();
+ case 'is_private' :
+ $response[ $key ] = $this->site->is_private();
+ 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 ( ! is_null( $icon ) ) {
+ $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 '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 '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 'quota' :
+ $response[ $key ] = $this->site->get_quota();
+ break;
+ case 'launch_status' :
+ $response[ $key ] = $this->site->get_launch_status();
+ break;
+ }
+
+ do_action( 'post_render_site_response_key', $key );
+ }
+
+ protected function render_option_keys( &$options_response_keys ) {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ return array();
+ }
+
+ $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 '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 '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 'design_type':
+ $options[ $key ] = $site->get_design_type();
+ break;
+ case 'site_goals':
+ $options[ $key ] = $site->get_site_goals();
+ break;
+ case 'site_segment':
+ $options[ $key ] = $site->get_site_segment();
+ break;
+ }
+ }
+
+ return $options;
+ }
+
+ 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
+ public function decorate_jetpack_response( &$response ) {
+ $this->site = $this->get_platform()->get_site( $response->ID );
+ switch_to_blog( $this->site->get_id() );
+
+ // ensure the response is marked as being from Jetpack
+ $response->jetpack = true;
+
+ $wpcom_response = $this->render_response_keys( self::$jetpack_response_field_additions );
+
+ foreach( $wpcom_response as $key => $value ) {
+ $response->{ $key } = $value;
+ }
+
+ if ( $this->has_blog_access( $this->api->token_details, $response->ID ) ) {
+ $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->capabilities );
+ unset( $response->lang );
+ unset( $response->user_can_manage );
+ unset( $response->is_multisite );
+ unset( $response->plan );
+ }
+
+ // render additional options
+ if ( $response->options ) {
+ $wpcom_options_response = $this->render_option_keys( self::$jetpack_response_option_additions );
+
+ 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,
+ ),
+
+ 'response_format' => array(
+ 'formats' => '(object) An object of supported post formats, each key a supported format slug mapped to its display string.',
+ )
+) );
+
+class WPCOM_JSON_API_List_Post_Formats_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/post-formats -> $blog_id
+ 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' );
+
+ $supported_formats = $response['formats'] = array();
+
+ 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'
+) );
+
+class WPCOM_JSON_API_List_Page_Templates_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/page-templates -> $blog_id
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-v1-2-endpoint.php
new file mode 100644
index 00000000..316b6282
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-site-v1-2-endpoint.php
@@ -0,0 +1,62 @@
+<?php
+
+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',
+ ),
+
+ 'query_parameters' => array(
+ 'context' => false,
+ ),
+
+ '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/',
+) );
+
+class WPCOM_JSON_API_GET_Site_V1_2_Endpoint extends WPCOM_JSON_API_GET_Site_Endpoint {
+
+ 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',
+ 'is_multisite' => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.',
+ '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',
+ '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',
+ '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.',
+ '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',
+ );
+
+
+ function callback( $path = '', $blog_id = 0 ) {
+ add_filter( 'sites_site_format', array( $this, 'site_format' ) );
+
+ return parent::callback( $path, $blog_id );
+ }
+
+ public function site_format( $format ) {
+ return self::$site_format;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomies-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomies-endpoint.php
new file mode 100644
index 00000000..80b2469b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomies-endpoint.php
@@ -0,0 +1,144 @@
+<?php
+
+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.',
+ ),
+ '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.',
+ ),
+ ),
+ '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'
+) );
+
+class WPCOM_JSON_API_Get_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/tags -> $blog_id
+ // /sites/%s/categories -> $blog_id
+ 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 );
+ }
+ }
+
+ 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;
+ }
+
+ function categories( $args ) {
+ $args['get'] = 'all';
+
+ $cats = get_categories( $args );
+ unset( $args['offset'] );
+ $found = wp_count_terms( 'category', $args );
+
+ $cats_obj = array();
+ foreach ( $cats as $cat ) {
+ $cats_obj[] = $this->format_taxonomy( $cat, 'category', 'display' );
+ }
+
+ return array(
+ 'found' => (int) $found,
+ 'categories' => $cats_obj
+ );
+ }
+
+ function tags( $args ) {
+ $args['get'] = 'all';
+
+ $tags = (array) get_tags( $args );
+ unset( $args['offset'] );
+ $found = wp_count_terms( 'post_tag', $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
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomy-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomy-endpoint.php
new file mode 100644
index 00000000..728fe463
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-taxonomy-endpoint.php
@@ -0,0 +1,59 @@
+<?php
+
+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'
+ ),
+
+ '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'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/slug:wordpresscom'
+) );
+
+class WPCOM_JSON_API_Get_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
+ // /sites/%s/tags/slug:%s -> $blog_id, $tag_id
+ // /sites/%s/categories/slug:%s -> $blog_id, $tag_id
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-term-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-term-endpoint.php
new file mode 100644
index 00000000..6546af74
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-get-term-endpoint.php
@@ -0,0 +1,54 @@
+<?php
+
+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.',
+ ),
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/taxonomies/post_tag/terms/slug:wordpresscom'
+) );
+
+class WPCOM_JSON_API_Get_Term_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-comments-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-comments-endpoint.php
new file mode 100644
index 00000000..429e0161
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-comments-endpoint.php
@@ -0,0 +1,309 @@
+<?php
+
+class WPCOM_JSON_API_List_Comments_Walker extends Walker {
+ public $tree_type = 'comment';
+
+ public $db_fields = array(
+ 'parent' => 'comment_parent',
+ 'id' => 'comment_ID'
+ );
+
+ public function start_el( &$output, $object, $depth = 0, $args = array(), $current_object_id = 0 ) {
+ 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()
+ */
+ 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',
+ ),
+
+ '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',
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/replies/?number=2'
+) );
+
+// @todo permissions
+class WPCOM_JSON_API_List_Comments_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
+ 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.',
+ );
+
+ 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.',
+ ),
+ ) );
+ }
+
+ // /sites/%s/comments/ -> $blog_id
+ // /sites/%s/posts/%d/replies/ -> $blog_id, $post_id
+ // /sites/%s/comments/%d/replies/ -> $blog_id, $comment_id
+ 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 ( false !== strpos( $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 = $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;
+ }
+ } else {
+ if ( $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 ( $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 );
+
+ update_comment_cache( $comments );
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-embeds-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-embeds-endpoint.php
new file mode 100644
index 00000000..6f2ce126
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-embeds-endpoint.php
@@ -0,0 +1,60 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_List_Embeds_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/embeds -> $blog_id
+ 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 $priority => $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;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-endpoint.php
new file mode 100644
index 00000000..95d11c58
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-endpoint.php
@@ -0,0 +1,82 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_List_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-1-endpoint.php
new file mode 100644
index 00000000..712909d9
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-1-endpoint.php
@@ -0,0 +1,288 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_List_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ public $date_range = array();
+ public $page_handle = array();
+
+ 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,
+ );
+
+ if ( isset( $args['page'] ) ) {
+ if ( $args['page'] < 1 ) {
+ $args['page'] = 1;
+ }
+
+ $query['paged'] = $args['page'];
+ if ( $query['paged'] !== 1 ) {
+ $is_eligible_for_page_handle = false;
+ }
+ } else {
+ if ( $args['offset'] < 0 ) {
+ $args['offset'] = 0;
+ }
+
+ $query['offset'] = $args['offset'];
+ if ( $query['offset'] !== 0 ) {
+ $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;
+ }
+
+ function build_page_handle( $post, $query ) {
+ $column = $query['orderby'];
+ if ( ! $column ) {
+ $column = 'date';
+ }
+ return build_query( array( 'value' => urlencode( $post->$column ), 'id' => $post->ID ) );
+ }
+
+ 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' ) ) ) {
+ return $where;
+ }
+
+ if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) {
+ 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'] );
+ if ( $db_column !== 'ID' ) {
+ $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'] );
+ }
+ $where .= ' )';
+
+ return $where;
+ }
+
+ 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;
+ }
+
+ function handle_orderby_for_page_handle( $orderby ) {
+ global $wpdb;
+ if ( $this->performed_query['orderby'] === 'ID' ) {
+ // 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;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-2-endpoint.php
new file mode 100644
index 00000000..0ffd971b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-media-v1-2-endpoint.php
@@ -0,0 +1,77 @@
+<?php
+
+jetpack_require_lib( 'class.media' );
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_List_Media_v1_2_Endpoint extends WPCOM_JSON_API_List_Media_v1_1_Endpoint {
+ function callback( $path = '', $blog_id = 0 ) {
+ $response = parent::callback( $path, $blog_id );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $media_list = $response['media'];
+
+ if ( count( $media_list ) < 1 ) {
+ return $response;
+ }
+
+ foreach ( $media_list as $index => $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;
+ }
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-type-taxonomies-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-type-taxonomies-endpoint.php
new file mode 100644
index 00000000..27b1cf0e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-type-taxonomies-endpoint.php
@@ -0,0 +1,98 @@
+<?php
+
+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'
+) );
+
+class WPCOM_JSON_API_List_Post_Type_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint {
+ static $taxonomy_keys_to_include = array(
+ 'name' => 'name',
+ 'label' => 'label',
+ 'labels' => 'labels',
+ 'description' => 'description',
+ 'hierarchical' => 'hierarchical',
+ 'public' => 'public',
+ 'cap' => 'capabilities',
+ );
+
+ // /sites/%s/post-types/%s/taxonomies -> $blog_id, $post_type
+ 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 );
+
+ $args = $this->query_args();
+
+ $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,
+ );
+ }
+
+ 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 );
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php
new file mode 100644
index 00000000..eb05630e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php
@@ -0,0 +1,117 @@
+<?php
+
+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',
+ ),
+
+ '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'
+) );
+
+class WPCOM_JSON_API_List_Post_Types_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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',
+ );
+
+ // /sites/%s/post-types -> $blog_id
+ 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();
+ }
+
+ $args = $this->query_args();
+
+ /**
+ * 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 re-register if localizing response
+ create_initial_post_types();
+ }
+
+ // 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
+ );
+ }
+
+ function post_type_supports_tags( $post_type ) {
+ if ( in_array( 'post_tag', get_object_taxonomies( $post_type ) ) ) {
+ 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'] );
+ }
+ return $post_type === $featured_content[0]['post_types'];
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-endpoint.php
new file mode 100644
index 00000000..726aaea4
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-endpoint.php
@@ -0,0 +1,353 @@
+<?php
+
+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',
+ ),
+
+ '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.',
+ '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'
+) );
+
+class WPCOM_JSON_API_List_Posts_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
+ public $date_range = 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.',
+ );
+
+ // /sites/%s/posts/ -> $blog_id
+ 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;
+ }
+ else {
+ if ( ! $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'] : null,
+ 'fields' => 'ids',
+ );
+
+ if ( ! is_user_logged_in () ) {
+ $query['has_password'] = false;
+ }
+
+ 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 (
+ isset( $args['sticky'] )
+ &&
+ ( $sticky = get_option( 'sticky_posts' ) )
+ &&
+ is_array( $sticky )
+ ) {
+ if ( $args['sticky'] ) {
+ $query['post__in'] = $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 ( $category === false) {
+ $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 ) ) {
+ $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;
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-1-endpoint.php
new file mode 100644
index 00000000..7094ffaf
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-1-endpoint.php
@@ -0,0 +1,529 @@
+<?php
+
+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',
+ ),
+
+ '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.',
+ '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'
+) );
+
+class WPCOM_JSON_API_List_Posts_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
+ public $date_range = array();
+ public $modified_range = array();
+ public $page_handle = array();
+ public $performed_query = null;
+
+ 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',
+ );
+
+ // /sites/%s/posts/ -> $blog_id
+ 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'] ) && ! $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();
+ }
+
+ if ( isset( $args['type'] ) &&
+ ! in_array( $args['type'], array( 'post', 'revision', 'page', 'any' ) ) &&
+ defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $this->load_theme_functions();
+ }
+
+ // 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;
+ }
+ else {
+ if ( ! $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'] : null,
+ 'fields' => 'ids',
+ );
+
+ if ( ! is_user_logged_in () ) {
+ $query['has_password'] = false;
+ }
+
+ 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 ( $args['sticky'] === 'include' ) {
+ $query['ignore_sticky_posts'] = 1;
+ } else if ( $args['sticky'] === 'exclude' ) {
+ $sticky = get_option( 'sticky_posts' );
+ if ( is_array( $sticky ) ) {
+ $query['post__not_in'] = $sticky;
+ }
+ } else if ( $args['sticky'] === 'require' ) {
+ $sticky = get_option( 'sticky_posts' );
+ if ( is_array( $sticky ) && ! empty( $sticky ) ) {
+ $query['post__in'] = $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 ( $category === false) {
+ $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 ( $query['paged'] !== 1 ) {
+ $is_eligible_for_page_handle = false;
+ }
+ } else {
+ if ( $args['offset'] < 0 ) {
+ $args['offset'] = 0;
+ }
+
+ $query['offset'] = $args['offset'];
+ if ( $query['offset'] !== 0 ) {
+ $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 ) ) {
+ $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'] );
+ if ( ( $return['found'] > count( $return['posts'] ) ) && $last_post ) {
+ if ( ! isset( $return[$key] ) ) {
+ $return[$key] = (object) array();
+ }
+ $return[$key]->next_page = $this->build_page_handle( $last_post, $query );
+ }
+ }
+
+ 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;
+ }
+
+ function build_page_handle( $post, $query ) {
+ $column = $query['orderby'];
+ if ( ! $column ) {
+ $column = 'date';
+ }
+ return build_query( array( 'value' => urlencode($post[$column]), 'id' => $post['ID'] ) );
+ }
+
+ 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 ) ",
+ $range['after'],
+ $range['before']
+ );
+ break;
+ case 1 :
+ if ( isset( $range['before'] ) ) {
+ $where .= $wpdb->prepare(
+ " AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",
+ $range['before']
+ );
+ } else {
+ $where .= $wpdb->prepare(
+ " AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ",
+ $range['after']
+ );
+ }
+ break;
+ }
+
+ return $where;
+ }
+
+ function handle_date_range( $where ) {
+ return $this->_build_date_range_query( 'post_date_gmt', $this->date_range, $where );
+ }
+
+ function handle_modified_range( $where ) {
+ return $this->_build_date_range_query( 'post_modified_gmt', $this->modified_range, $where );
+ }
+
+ 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' ) ) ) {
+ return $where;
+ }
+
+ if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) {
+ 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'] );
+ if ( $db_column !== 'ID' ) {
+ $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'] );
+ }
+ $where .= ' )';
+
+ return $where;
+ }
+
+ function handle_orderby_for_page_handle( $orderby ) {
+ global $wpdb;
+ if ( $this->performed_query['orderby'] === 'ID' ) {
+ // 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-2-endpoint.php
new file mode 100644
index 00000000..3aa61bb5
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-posts-v1-2-endpoint.php
@@ -0,0 +1,433 @@
+<?php
+
+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',
+ ),
+
+ '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.',
+ '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'
+) );
+
+class WPCOM_JSON_API_List_Posts_v1_2_Endpoint extends WPCOM_JSON_API_List_Posts_v1_1_Endpoint {
+ // /sites/%s/posts/ -> $blog_id
+ 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' ) ) ) {
+ $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 ( isset( $args['exclude_private_types'] ) && $args['exclude_private_types'] == true ) {
+ $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;
+ }
+ else {
+ if ( ! $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'] : null,
+ 'fields' => 'ids',
+ );
+
+ if ( ! is_user_logged_in () ) {
+ $query['has_password'] = false;
+ }
+
+ 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 ( $args['sticky'] === 'include' ) {
+ $query['ignore_sticky_posts'] = 1;
+ } else if ( $args['sticky'] === 'exclude' ) {
+ $sticky = get_option( 'sticky_posts' );
+ if ( is_array( $sticky ) ) {
+ $query['post__not_in'] = $sticky;
+ }
+ } else if ( $args['sticky'] === 'require' ) {
+ $sticky = get_option( 'sticky_posts' );
+ if ( is_array( $sticky ) && ! empty( $sticky ) ) {
+ $query['post__in'] = $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 ( $category === false) {
+ $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 ( $query['paged'] !== 1 ) {
+ $is_eligible_for_page_handle = false;
+ }
+ } else {
+ if ( $args['offset'] < 0 ) {
+ $args['offset'] = 0;
+ }
+
+ $query['offset'] = $args['offset'];
+ if ( $query['offset'] !== 0 ) {
+ $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 ) ) {
+ $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'] );
+ if ( ( $return['found'] > count( $return['posts'] ) ) && $last_post ) {
+ if ( ! isset( $return[$key] ) ) {
+ $return[$key] = (object) array();
+ }
+ $return[$key]->next_page = $this->build_page_handle( $last_post, $query );
+ }
+ }
+
+ 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;
+ }
+
+ function build_page_handle( $post, $query ) {
+ $column = $query['orderby'];
+ if ( ! $column ) {
+ $column = 'date';
+ }
+ return build_query( array( 'value' => urlencode($post[$column]), 'id' => $post['ID'] ) );
+ }
+
+ 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 ) ",
+ $range['after'],
+ $range['before']
+ );
+ break;
+ case 1 :
+ if ( isset( $range['before'] ) ) {
+ $where .= $wpdb->prepare(
+ " AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",
+ $range['before']
+ );
+ } else {
+ $where .= $wpdb->prepare(
+ " AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ",
+ $range['after']
+ );
+ }
+ break;
+ }
+
+ return $where;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-roles-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-roles-endpoint.php
new file mode 100644
index 00000000..733f26f7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-roles-endpoint.php
@@ -0,0 +1,135 @@
+<?php
+
+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',
+ ),
+ ),
+) );
+
+class WPCOM_JSON_API_List_Roles_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ var $response_format = array(
+ 'roles' => '(array:role) Array of role objects',
+ );
+
+ 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 );
+ $b_is_core_role = in_array( $b->name, $core_role_names );
+
+ // if $a is a core_role and $b is not, $a always comes first
+ if ( $a_is_core_role && ! $b_is_core_role ) {
+ return -1;
+ }
+
+ // if $b is a core_role and $a is not, $b always comes first
+ if ( $b_is_core_role && ! $a_is_core_role ) {
+ return 1;
+ }
+
+ // otherwise the one with the > number of capabilities comes first
+ $a_cap_count = count( $a->capabilities );
+ $b_cap_count = count( $b->capabilities );
+
+ if ( $a_cap_count === $b_cap_count ) {
+ return 0;
+ }
+
+ return ( $a_cap_count > $b_cap_count ) ? -1 : 1;
+ }
+
+ // /sites/%s/roles/ -> $blog_id
+ 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 ( method_exists( $wp_roles, 'get_names' ) ) {
+ $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;
+ }
+ } else {
+ // 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']
+ );
+ }
+ }
+
+ // Sort the array so roles with the most number of capabilities comes first, then the next role, and so on
+ usort( $roles, array( 'self', 'role_sort' ) );
+
+ return array( 'roles' => $roles );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-shortcodes-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-shortcodes-endpoint.php
new file mode 100644
index 00000000..ee21bd33
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-shortcodes-endpoint.php
@@ -0,0 +1,48 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_List_Shortcodes_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/shortcodes -> $blog_id
+ 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;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-terms-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-terms-endpoint.php
new file mode 100644
index 00000000..83edcfc9
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-terms-endpoint.php
@@ -0,0 +1,112 @@
+<?php
+
+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.',
+ ),
+ ),
+ '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'
+) );
+
+class WPCOM_JSON_API_List_Terms_Endpoint extends WPCOM_JSON_API_Endpoint {
+ // /sites/%s/taxonomies/%s/terms -> $blog_id, $taxonomy
+ 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', count( $formatted_terms ) );
+ }
+
+ return array(
+ 'found' => (int) $this->get_found( $taxonomy, $args ),
+ 'terms' => (array) $formatted_terms
+ );
+ }
+
+ 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;
+ }
+
+ function get_found( $taxonomy, $args ) {
+ unset( $args['offset'] );
+ return wp_count_terms( $taxonomy, $args );
+ }
+
+ function get_formatted_terms( $taxonomy, $args ) {
+ $terms = get_terms( $taxonomy, $args );
+
+ $formatted_terms = array();
+ foreach ( $terms as $term ) {
+ $formatted_terms[] = $this->format_taxonomy( $term, $taxonomy, 'display' );
+ }
+
+ return $formatted_terms;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php
new file mode 100644
index 00000000..489223b7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php
@@ -0,0 +1,177 @@
+<?php
+
+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',
+ '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.'
+ ),
+
+ '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://en.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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_List_Users_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ var $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',
+ );
+
+ // /sites/%s/users/ -> $blog_id
+ 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['who'] = 'authors';
+ }
+
+ 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'];
+ }
+
+ $user_query = new WP_User_Query( $query );
+
+ remove_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ) );
+
+ $return = array();
+ foreach ( array_keys( $this->response_format ) as $key ) {
+ switch ( $key ) {
+ case 'found' :
+ $return[ $key ] = (int) $user_query->get_total();
+ 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;
+ }
+ }
+
+ $return[ $key ] = $users;
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ function api_user_override_search_columns( $search_columns, $search ) {
+ return $this->search_columns;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php
new file mode 100644
index 00000000..be02c6c1
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php
@@ -0,0 +1,824 @@
+<?php
+abstract class WPCOM_JSON_API_Menus_Abstract_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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;
+ }
+
+
+ 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; $i < count( $locations ); $i++ ) {
+ $locations[ $i ]['defaultState'] = $i ? 'empty' : 'default';
+ }
+ return $locations;
+ }
+
+ protected function simplify( $data ) {
+ $simplifier = new WPCOM_JSON_API_Menus_Simplifier( $data );
+ return $simplifier->translate();
+ }
+
+ protected function complexify( $data ) {
+ $complexifier = new WPCOM_JSON_API_Menus_Complexify( $data );
+ return $complexifier->translate();
+ }
+}
+
+abstract class WPCOM_JSON_API_Menus_Translator {
+ protected $filter = '';
+
+ protected $filters = array();
+
+ public function __construct( $menus ) {
+ $this->is_single_menu = ! is_array( $menus );
+ $this->menus = is_array( $menus ) ? $menus : array( $menus );
+ }
+
+ 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 );
+ }
+
+ protected function maybe_extract( $menus ) {
+ return $this->is_single_menu ? $menus[0] : $menus;
+ }
+
+ public function whitelist_and_rename_with( $object, $dict ) {
+ $keys = array_keys( $dict );
+ $return = array();
+ foreach ( (array) $object as $k => $v ) {
+ if ( in_array( $k, $keys ) ) {
+ 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;
+ }
+}
+
+class WPCOM_JSON_API_Menus_Simplifier extends WPCOM_JSON_API_Menus_Translator {
+ protected $filter = 'wpcom_menu_api_translator_simplify';
+
+ protected $filters = array(
+ 'whitelist_and_rename_keys',
+ 'add_locations',
+ 'treeify',
+ 'add_widget_locations',
+ );
+
+ 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' ),
+ );
+
+ 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
+ **************************/
+
+ public function treeify( $menus ) {
+ return array_map( array( $this, 'treeify_menu' ), $menus );
+ }
+
+ // turn the flat item list into a tree of items
+ 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
+ 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'] );
+ }
+
+ 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;
+ }
+
+ protected function add_locations( $menus ) {
+ $menus_with_locations = array();
+
+ foreach( $menus as $menu ) {
+ $menu['locations'] = array_keys( get_nav_menu_locations(), $menu['id'] );
+ $menus_with_locations[] = $menu;
+ }
+
+ return $menus_with_locations;
+ }
+
+ 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;
+ }
+}
+
+class WPCOM_JSON_API_Menus_Complexify extends WPCOM_JSON_API_Menus_Translator {
+ protected $filter = 'wpcom_menu_api_translator_complexify';
+
+ protected $filters = array(
+ 'untreeify',
+ 'set_locations',
+ 'whitelist_and_rename_keys',
+ );
+
+ protected $menu_whitelist = array(
+ 'id' => 'term_id',
+ 'name' => 'menu-name',
+ 'description' => 'description',
+ 'items' => 'items',
+ );
+
+ 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
+ **************************/
+
+ 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
+ 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().
+ */
+ 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;
+ }
+ }
+
+ 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;
+ }
+
+ 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
+ protected function implode_array_fields( $menu_item ) {
+ return array_map( array( $this, 'implode_array_field' ), $menu_item );
+ }
+
+ protected function implode_array_field( $field ) {
+ if ( is_array( $field ) ) {
+ return implode( ' ', $field );
+ }
+ return $field;
+ }
+
+ 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 );
+ }
+
+ protected function set_location( $menu ) {
+ $this->set_menu_at_locations( $menu['locations'], $menu['id'] );
+ return $menu;
+ }
+
+ 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 );
+ }
+
+ 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] );
+ }
+ }
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+}
+
+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'
+ )
+ ),
+) );
+
+class WPCOM_JSON_API_Menus_New_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
+ 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'
+ ),
+ ),
+) );
+
+class WPCOM_JSON_API_Menus_Update_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
+ 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.
+ */
+ 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.
+ */
+ 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' ),
+ ),
+) );
+
+class WPCOM_JSON_API_Menus_List_Menus_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
+ 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' ),
+ ),
+) );
+
+class WPCOM_JSON_API_Menus_Get_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
+ 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' ),
+ ),
+) );
+
+class WPCOM_JSON_API_Menus_Delete_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
+ 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;
+ }
+}
+
+class WPCOM_JSON_API_Menus_Widgets {
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-endpoint.php
new file mode 100644
index 00000000..713fa68a
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-endpoint.php
@@ -0,0 +1,658 @@
+<?php
+
+abstract class WPCOM_JSON_API_Post_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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 Twitter and 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',
+ );
+
+ // public $response_format =& $this->post_object_format;
+
+ 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 );
+ }
+
+ function the_password_form() {
+ return __( 'This post is password protected.', 'jetpack' );
+ }
+
+ /**
+ * Get a post by a specified field and value
+ *
+ * @param string $field
+ * @param string $field_value
+ * @param string $context Post use context (e.g. 'display')
+ * @return array Post
+ **/
+ 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'];
+ }
+ }
+
+ if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'wp-windows8' ) ) {
+ remove_shortcode( 'gallery', 'gallery_shortcode' );
+ add_shortcode( 'gallery', array( &$this, 'win8_gallery_shortcode' ) );
+ }
+
+ 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;
+
+ 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;
+ case 'parent' : // (object|false)
+ 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;
+ case 'geo' : // (object|false)
+ 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 ) {
+ 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;
+ case 'metadata' : // (array|false)
+ $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;
+
+ // Only business plan subscribers can view custom meta description.
+ if ( Jetpack_SEO_Posts::DESCRIPTION_META_KEY === $meta['meta_key'] && ! 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;
+
+ }
+ }
+
+ // WPCOM_JSON_API_Post_Endpoint::find_featured_worthy_media( $post );
+ // $response['featured_media'] = self::find_featured_media( $response );
+
+ unset( $GLOBALS['post'] );
+ return $response;
+ }
+
+ // No Blog ID parameter. No Post ID parameter. Depends on globals.
+ // Expects setup_postdata() to already have been run
+ function get_the_post_content_for_display() {
+ global $pages, $page;
+
+ $old_pages = $pages;
+ $old_page = $page;
+
+ $content = join( "\n\n", $pages );
+ $content = preg_replace( '/<!--more(.*?)?-->/', '', $content );
+ $pages = array( $content );
+ $page = 1;
+
+ ob_start();
+ the_content();
+ $return = ob_get_clean();
+
+ $pages = $old_pages;
+ $page = $old_page;
+
+ return $return;
+ }
+
+ 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
+ * @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();
+ }
+
+ }
+
+
+
+ function win8_gallery_shortcode( $attr ) {
+ global $post;
+
+ static $instance = 0;
+ $instance++;
+
+ $output = '';
+
+ // We're trusting author input, so let's at least make sure it looks like a valid orderby statement
+ if ( isset( $attr['orderby'] ) ) {
+ $attr['orderby'] = sanitize_sql_orderby( $attr['orderby'] );
+ if ( !$attr['orderby'] )
+ unset( $attr['orderby'] );
+ }
+
+ extract( shortcode_atts( array(
+ 'order' => 'ASC',
+ 'orderby' => 'menu_order ID',
+ 'id' => $post->ID,
+ 'include' => '',
+ 'exclude' => '',
+ 'slideshow' => false
+ ), $attr, 'gallery' ) );
+
+ // Custom image size and always use it
+ add_image_size( 'win8app-column', 480 );
+ $size = 'win8app-column';
+
+ $id = intval( $id );
+ if ( 'RAND' === $order )
+ $orderby = 'none';
+
+ if ( !empty( $include ) ) {
+ $include = preg_replace( '/[^0-9,]+/', '', $include );
+ $_attachments = get_posts( array( 'include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ $attachments = array();
+ foreach ( $_attachments as $key => $val ) {
+ $attachments[$val->ID] = $_attachments[$key];
+ }
+ } elseif ( !empty( $exclude ) ) {
+ $exclude = preg_replace( '/[^0-9,]+/', '', $exclude );
+ $attachments = get_children( array( 'post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ } else {
+ $attachments = get_children( array( 'post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ }
+
+ if ( ! empty( $attachments ) ) {
+ foreach ( $attachments as $id => $attachment ) {
+ $link = isset( $attr['link'] ) && 'file' === $attr['link'] ? wp_get_attachment_link( $id, $size, false, false ) : wp_get_attachment_link( $id, $size, true, false );
+
+ if ( $captiontag && trim($attachment->post_excerpt) ) {
+ $output .= "<div class='wp-caption aligncenter'>$link
+ <p class='wp-caption-text'>" . wptexturize($attachment->post_excerpt) . "</p>
+ </div>";
+ } else {
+ $output .= $link . ' ';
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns attachment object.
+ *
+ * @param $attachment attachment row
+ *
+ * @return object
+ */
+ 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 post object
+ * @return array array of post-level permissions; 'publish_post', 'delete_post', 'edit_post'
+ */
+ function get_current_user_capabilities( $post ) {
+ return array(
+ 'publish_post' => current_user_can( 'publish_post', $post ),
+ 'delete_post' => current_user_can( 'delete_post', $post ),
+ 'edit_post' => current_user_can( 'edit_post', $post )
+ );
+ }
+
+ /**
+ * Get post ID by name
+ *
+ * Attempts to match name on post title and page path
+ *
+ * @param string $name
+ *
+ * @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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-v1-1-endpoint.php
new file mode 100644
index 00000000..418cfb16
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-post-v1-1-endpoint.php
@@ -0,0 +1,353 @@
+<?php
+
+abstract class WPCOM_JSON_API_Post_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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 Twitter and 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.',
+ );
+
+ // public $response_format =& $this->post_object_format;
+
+ 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
+ * @param string $field_value
+ * @param string $context Post use context (e.g. 'display')
+ * @return array Post
+ **/
+ function get_post_by( $field, $field_value, $context = 'display' ) {
+
+ // validate input
+ if ( ! in_array( $field, array( 'ID', 'name' ) ) ) {
+ return new WP_Error( 'invalid_field', 'Invalid API FIELD', 400 );
+ }
+
+ if ( ! in_array( $context, array( 'display', 'edit' ) ) ) {
+ 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'];
+ }
+ }
+
+ if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'wp-windows8' ) ) {
+ remove_shortcode( 'gallery', 'gallery_shortcode' );
+ add_shortcode( 'gallery', array( &$this, 'win8_gallery_shortcode' ) );
+ }
+
+ // fetch SAL post
+ $post = $this->get_sal_post_by( $field, $field_value, $context );
+
+ if ( is_wp_error( $post ) ) {
+ return $post;
+ }
+
+ $GLOBALS['post'] = $post;
+
+ // 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;
+ }
+
+ 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;
+ }
+
+ private function render_response_keys( $post, $context, $keys ) {
+ 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;
+ case 'parent' : // (object|false)
+ $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;
+ case 'geo' : // (object|false)
+ $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;
+ case 'metadata' : // (array|false)
+ $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;
+ }
+
+ // TODO: factor this out
+ 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;
+ }
+
+ function win8_gallery_shortcode( $attr ) {
+ global $post;
+
+ static $instance = 0;
+ $instance++;
+
+ $output = '';
+
+ // We're trusting author input, so let's at least make sure it looks like a valid orderby statement
+ if ( isset( $attr['orderby'] ) ) {
+ $attr['orderby'] = sanitize_sql_orderby( $attr['orderby'] );
+ if ( !$attr['orderby'] )
+ unset( $attr['orderby'] );
+ }
+
+ extract( shortcode_atts( array(
+ 'order' => 'ASC',
+ 'orderby' => 'menu_order ID',
+ 'id' => $post->ID,
+ 'include' => '',
+ 'exclude' => '',
+ 'slideshow' => false
+ ), $attr, 'gallery' ) );
+
+ // Custom image size and always use it
+ add_image_size( 'win8app-column', 480 );
+ $size = 'win8app-column';
+
+ $id = intval( $id );
+ if ( 'RAND' === $order )
+ $orderby = 'none';
+
+ if ( !empty( $include ) ) {
+ $include = preg_replace( '/[^0-9,]+/', '', $include );
+ $_attachments = get_posts( array( 'include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ $attachments = array();
+ foreach ( $_attachments as $key => $val ) {
+ $attachments[$val->ID] = $_attachments[$key];
+ }
+ } elseif ( !empty( $exclude ) ) {
+ $exclude = preg_replace( '/[^0-9,]+/', '', $exclude );
+ $attachments = get_children( array( 'post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ } else {
+ $attachments = get_children( array( 'post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
+ }
+
+ if ( ! empty( $attachments ) ) {
+ foreach ( $attachments as $id => $attachment ) {
+ $link = isset( $attr['link'] ) && 'file' === $attr['link'] ? wp_get_attachment_link( $id, $size, false, false ) : wp_get_attachment_link( $id, $size, true, false );
+
+ if ( $captiontag && trim($attachment->post_excerpt) ) {
+ $output .= "<div class='wp-caption aligncenter'>$link
+ <p class='wp-caption-text'>" . wptexturize($attachment->post_excerpt) . "</p>
+ </div>";
+ } else {
+ $output .= $link . ' ';
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-endpoint.php
new file mode 100644
index 00000000..1c08060b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-endpoint.php
@@ -0,0 +1,75 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Render_Embed_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
+ // /sites/%s/embeds/render -> $blog_id
+ 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 ( 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 = 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['embed_url'] = $embed_url;
+ $return['result'] = $render['result'];
+
+ $return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] );
+
+ return $return;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-reversal-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-reversal-endpoint.php
new file mode 100644
index 00000000..32af88da
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-embed-reversal-endpoint.php
@@ -0,0 +1,108 @@
+<?php
+
+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' => 'sites',
+ '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&amp;wmode=direct" scrolling="no" frameborder="0"></iframe>',
+ )
+ ),
+) );
+
+class WPCOM_JSON_API_Render_Embed_Reversal_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
+ // /sites/%s/embeds/reversal -> $blog_id
+ 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_shortcode = $is_embed = 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 );
+
+ if ( ( count( $shortcode_matches[0] ) + count( $url_matches[0] ) ) > 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['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;
+ }
+
+ function render_shortcode_reversal( $args ) {
+ if ( $args['is_shortcode'] ) {
+ return call_user_func( array( $this, 'do_shortcode' ), $args['shortcode_reversal'] );
+ } else if ( $args['is_embed'] ) {
+ return call_user_func( array( $this, 'do_embed' ), $args['shortcode_reversal'] );
+ }
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-endpoint.php
new file mode 100644
index 00000000..f2de7fc3
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-endpoint.php
@@ -0,0 +1,145 @@
+<?php
+
+// 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)
+ */
+ function process_render( $callback, $callback_arg ) {
+ global $wp_scripts, $wp_styles;
+
+ // 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
+ */
+ function add_assets( $return, $loaded_scripts, $loaded_styles ) {
+ global $wp_scripts, $wp_styles;
+ // scripts first, just cuz
+ if ( count( $loaded_scripts ) > 0 ) {
+ $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 . '&amp;' . $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 ( count( $loaded_styles ) > 0 ) {
+ $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 . '&amp;' . $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
+ */
+ 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
+ */
+ function do_shortcode( $shortcode ) {
+ return do_shortcode( $shortcode );
+ }
+
+ /**
+ * given a one-line embed URL, process and return the result
+ */
+ function do_embed( $embed_url ) {
+ // in order for oEmbed to fire in the `$wp_embed->shortcode` method, we need to set a post as the current post
+ $_posts = get_posts( array( 'posts_per_page' => 1, 'suppress_filters' => false ) );
+ if ( ! empty( $_posts ) ) {
+ global $post;
+ $post = array_shift( $_posts );
+ }
+
+ global $wp_embed;
+ return $wp_embed->shortcode( array(), $embed_url );
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-shortcode-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-shortcode-endpoint.php
new file mode 100644
index 00000000..395fc7ac
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-render-shortcode-endpoint.php
@@ -0,0 +1,71 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Render_Shortcode_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
+ // /sites/%s/shortcodes/render -> $blog_id
+ 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 ( 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['shortcode'] = $shortcode;
+ $return['result'] = $render['result'];
+
+ $return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] );
+
+ return $return;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-sharing-buttons-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-sharing-buttons-endpoint.php
new file mode 100644
index 00000000..53150df6
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-sharing-buttons-endpoint.php
@@ -0,0 +1,644 @@
+<?php
+
+abstract class WPCOM_JSON_API_Sharing_Button_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ public static $all_visibilities = array( 'visible', 'hidden' );
+
+ protected $sharing_service;
+
+ 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 );
+ } else if ( ! 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 );
+ }
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ public function is_button_enabled( $button ) {
+ return false !== $this->get_button_visibility( $button );
+ }
+
+ 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'] );
+ }
+
+ protected function validate_button_input( $button, $is_new = false ) {
+ if ( ! empty( $button['visibility'] ) && ! in_array( $button['visibility'], self::$all_visibilities ) ) {
+ return new WP_Error( 'invalid_visibility', sprintf( 'The visibility field must be one of the following values: %s', implode( ', ', self::$all_visibilities ) ), 400 );
+ } else if ( $is_new && empty( $button['URL'] ) ) {
+ return new WP_Error( 'invalid_request', 'The URL field is required', 400 );
+ } else if ( $is_new && empty( $button['icon'] ) ) {
+ return new WP_Error( 'invalid_request', 'The icon field is required', 400 );
+ }
+ }
+
+ 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;
+ }
+
+ 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';
+ } else if ( 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"
+ }
+ ]
+}'
+) );
+
+class WPCOM_JSON_API_Get_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
+
+ // GET /sites/%s/sharing-buttons -> $blog_id
+ 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 ) ) {
+ 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"
+}'
+) );
+
+class WPCOM_JSON_API_Get_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
+
+ // GET /sites/%s/sharing-buttons/%s -> $blog_id, $button_id
+ 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"
+ }
+ ]
+}'
+) );
+
+class WPCOM_JSON_API_Update_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
+
+ // POST /sites/%s/sharing-buttons -> $blog_id
+ 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%"
+}'
+) );
+
+class WPCOM_JSON_API_Update_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
+
+ // POST /sites/%s/sharing-buttons/new -> $blog_id
+ // POST /sites/%s/sharing-buttons/%s -> $blog_id, $button_id
+ 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 );
+ } else if ( 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"
+}'
+) );
+
+class WPCOM_JSON_API_Delete_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
+
+ // POST /sites/%s/sharing-buttons/%s/delete -> $blog_id, $button_id
+ 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
+ );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php
new file mode 100644
index 00000000..d55a7e9b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php
@@ -0,0 +1,863 @@
+<?php
+
+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',
+ '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_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 whitelist',
+ '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 follows my blog?',
+ 'comment_moderation' => '(bool) Moderate comments for manual approval?',
+ 'comment_whitelist' => '(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',
+ 'blacklist_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',
+ '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',
+ 'amp_is_enabled' => '(bool) Whether AMP is enabled for this site',
+ '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',
+ ),
+
+ 'response_format' => array(
+ 'updated' => '(array)'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings',
+) );
+
+class WPCOM_JSON_API_Site_Settings_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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.',
+ );
+
+ // GET /sites/%s/settings
+ // POST /sites/%s/settings
+ 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 );
+ } else if ( ! 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();
+ } else if ( '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
+ */
+ 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 ) );
+ }
+
+ /**
+ * Returns category details
+ *
+ * @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 ( is_null( $option_value ) ) {
+ 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() {
+
+ // 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();
+ /** This filter is documented in class.json-api-endpoints.php */
+ $is_jetpack = true === apply_filters( 'is_jetpack_site', false, $blog_id );
+
+ foreach ( array_keys( $response_format ) as $key ) {
+
+ // refactoring to change lang parameter to locale in 1.2
+ if ( $lang_or_locale = $this->get_locale( $key ) ) {
+ $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;
+ }
+
+ if ( method_exists( 'Jetpack', 'is_module_active' ) ) {
+ $jetpack_relatedposts_options[ 'enabled' ] = Jetpack::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 = false;
+ if ( method_exists( 'Jetpack', 'is_module_active' ) ) {
+ $jetpack_search_active = Jetpack::is_module_active( 'search' );
+ }
+ if ( function_exists( 'is_jetpack_module_active' ) ) {
+ $jetpack_search_active = is_jetpack_module_active( 'search', $blog_id );
+ }
+
+ // 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 ) )
+ )
+ );
+
+ $api_cache = $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' => (bool) ( 'closed' != get_option( 'default_ping_status' ) ),
+ 'default_comment_status' => (bool) ( 'closed' != get_option( 'default_comment_status' ) ),
+
+ // new stuff starts here
+ 'blog_public' => (int) get_option( 'blog_public' ),
+ '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_headline' => (bool) isset( $jetpack_relatedposts_options[ 'show_headline' ] ) ? $jetpack_relatedposts_options[ 'show_headline' ] : false,
+ 'jetpack_relatedposts_show_thumbnails' => (bool) isset( $jetpack_relatedposts_options[ 'show_thumbnails' ] ) ? $jetpack_relatedposts_options[ 'show_thumbnails' ] : false,
+ '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_whitelist' ),
+ 'comment_max_links' => (int) get_option( 'comment_max_links' ),
+ 'moderation_keys' => get_option( 'moderation_keys' ),
+ 'blacklist_keys' => get_option( 'blacklist_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' ),
+ 'wga' => $this->get_google_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' ),
+ '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() ),
+ 'amp_is_supported' => (bool) function_exists( 'wpcom_is_amp_supported' ) && wpcom_is_amp_supported( $blog_id ),
+ 'amp_is_enabled' => (bool) function_exists( 'wpcom_is_amp_enabled' ) && wpcom_is_amp_enabled( $blog_id ),
+ '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' ),
+ );
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $response[ $key ]['wpcom_publish_posts_with_markdown'] = (bool) WPCom_Markdown::is_posting_enabled();
+ $response[ $key ]['wpcom_publish_comments_with_markdown'] = (bool) WPCom_Markdown::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();
+
+ $infinite_scroll_settings = The_Neverending_Home_Page::get_settings();
+ $response[ $key ]['infinite_scroll'] = get_option( 'infinite_scroll', true ) && $infinite_scroll_settings->type === 'scroll';
+ 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
+ *
+ * @param mixed $response_item A single site setting.
+ */
+ $response[ $key ] = apply_filters( 'site_settings_endpoint_get', $response[ $key ] );
+
+ 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'];
+ }
+
+ if ( function_exists( 'jetpack_protect_format_whitelist' ) ) {
+ $response[ $key ]['jetpack_protect_whitelist'] = jetpack_protect_format_whitelist();
+ }
+
+ if ( ! current_user_can( 'edit_posts' ) )
+ unset( $response[$key] );
+ break;
+ }
+ }
+
+ return $response;
+
+ }
+
+ 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;
+ }
+
+ protected function get_google_analytics () {
+ $option_name = defined( 'IS_WPCOM' ) && IS_WPCOM ? 'wga' : 'jetpack_wga';
+ return get_option( $option_name );
+ }
+
+ /**
+ * Updates site settings for authorized users
+ *
+ * @return array
+ */
+ 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 );
+ }
+ $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 'jetpack_protect_whitelist':
+ if ( function_exists( 'jetpack_protect_save_whitelist' ) ) {
+ $result = jetpack_protect_save_whitelist( $value );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ $updated[ $key ] = jetpack_protect_format_whitelist();
+ }
+ break;
+ case 'jetpack_sync_non_public_post_stati':
+ Jetpack_Options::update_option( 'sync_non_public_post_stati', $value );
+ break;
+ case 'jetpack_search_enabled':
+ if ( ! method_exists( 'Jetpack', 'activate_module' ) ) {
+ break;
+ }
+ $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
+ if ( $value ) {
+ $jetpack_search_update_success = $is_wpcom
+ ? Jetpack::activate_module( $blog_id, 'search' )
+ : Jetpack::activate_module( 'search', false, false );
+ } else {
+ $jetpack_search_update_success = $is_wpcom
+ ? Jetpack::deactivate_module( $blog_id, 'search' )
+ : Jetpack::deactivate_module( 'search' );
+ }
+ $updated[ $key ] = (bool) $value;
+ break;
+ case 'jetpack_relatedposts_enabled':
+ case 'jetpack_relatedposts_show_thumbnails':
+ case 'jetpack_relatedposts_show_headline':
+ if ( ! $this->jetpack_relatedposts_supported() ) {
+ break;
+ }
+ if ( 'jetpack_relatedposts_enabled' === $key && method_exists( 'Jetpack', 'is_module_active' ) && $this->jetpack_relatedposts_supported() ) {
+ $before_action = Jetpack::is_module_active('related-posts');
+ if ( $value ) {
+ Jetpack::activate_module( 'related-posts', false, false );
+ } else {
+ Jetpack::deactivate_module( 'related-posts' );
+ }
+ $after_action = Jetpack::is_module_active('related-posts');
+ if ( $after_action == $before_action ) {
+ break;
+ }
+ }
+ $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 'wga':
+ case 'jetpack_wga':
+ if ( ! isset( $value['code'] ) || ! preg_match( '/^$|^UA-[\d-]+$/i', $value['code'] ) ) {
+ return new WP_Error( 'invalid_code', 'Invalid UA ID' );
+ }
+
+ $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
+ $option_name = $is_wpcom ? 'wga' : 'jetpack_wga';
+
+ $wga = get_option( $option_name, array() );
+ $wga['code'] = $value['code']; // maintain compatibility with wp-google-analytics
+
+ /**
+ * Allow newer versions of this endpoint to filter in additional fields for Google Analytics
+ *
+ * @since 5.4.0
+ *
+ * @param array $wga Associative array of existing Google Analytics settings.
+ * @param array $value Associative array of new Google Analytics settings passed to the endpoint.
+ */
+ $wga = apply_filters( 'site_settings_update_wga', $wga, $value );
+
+ if ( update_option( $option_name, $wga ) ) {
+ $updated[ $key ] = $value;
+ }
+
+ $enabled_or_disabled = $wga['code'] ? 'enabled' : 'disabled';
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'google-analytics', $enabled_or_disabled );
+
+ if ( $is_wpcom ) {
+ $business_plugins = WPCOM_Business_Plugins::instance();
+ $business_plugins->activate_plugin( 'wp-google-analytics' );
+ }
+ break;
+
+ case 'jetpack_testimonial':
+ case 'jetpack_portfolio':
+ case 'jetpack_comment_likes_enabled':
+ // 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;
+ }
+ } else if ( 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;
+ }
+ } else if ( 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 'date_format':
+ case 'time_format':
+ // settings are stored as strings
+ if ( update_option( $key, sanitize_text_field( $value ) ) ) {
+ $updated[ $key ] = $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;
+ }
+ } else if ( 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_grandfathered_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() ) {
+ 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 'amp_is_enabled':
+ if ( function_exists( 'wpcom_update_amp_enabled' ) ) {
+ $saved = wpcom_update_amp_enabled( $blog_id, $value );
+ if ( $saved ) {
+ $updated[ $key ] = (bool) $value;
+ }
+ }
+ break;
+
+ case 'rss_use_excerpt':
+ update_option( 'rss_use_excerpt', (int)(bool) $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
+ *
+ * @param mixed $response_item A single site setting value.
+ */
+ $value = apply_filters( 'site_settings_endpoint_update_' . $key, $value );
+ $updated[ $key ] = $value;
+ break;
+ }
+
+ // no worries, we've already whitelisted and casted arguments above
+ if ( update_option( $key, $value ) ) {
+ $updated[ $key ] = $value;
+ }
+ }
+ }
+
+ if ( count( $jetpack_relatedposts_options ) ) {
+ // track new jetpack_relatedposts options against old
+ $old_relatedposts_options = Jetpack_Options::get_option( 'relatedposts' );
+ if ( Jetpack_Options::update_option( 'relatedposts', $jetpack_relatedposts_options ) ) {
+ foreach ( $jetpack_relatedposts_options as $key => $value ) {
+ if ( 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
+ );
+
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-2-endpoint.php
new file mode 100644
index 00000000..989bba2e
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-2-endpoint.php
@@ -0,0 +1,164 @@
+<?php
+
+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_headline' => '(bool) Show headline in related posts?',
+ 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
+ 'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
+ 'jetpack_search_supported' => '(bool) Jetpack Search supported',
+ 'jetpack_protect_whitelist' => '(array) List of IP addresses to whitelist',
+ '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 follows my blog?',
+ 'comment_moderation' => '(bool) Moderate comments for manual approval?',
+ 'comment_whitelist' => '(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',
+ 'blacklist_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',
+ 'amp_is_enabled' => '(bool) Whether AMP is enabled for this site',
+ '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',
+ ),
+
+ 'response_format' => array(
+ 'updated' => '(array)'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1',
+) );
+
+class WPCOM_JSON_API_Site_Settings_V1_2_Endpoint extends WPCOM_JSON_API_Site_Settings_Endpoint {
+
+ 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.',
+ );
+
+
+ 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 );
+ }
+
+
+ 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;
+ }
+
+ public function return_locale( $settings ) {
+ return $settings + array( 'locale' => $this->get_locale( 'locale' ) );
+ }
+
+ 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;
+ }
+
+ public function site_format( $format ) {
+ return self::$site_format;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-3-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-3-endpoint.php
new file mode 100644
index 00000000..52a3a148
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-3-endpoint.php
@@ -0,0 +1,157 @@
+<?php
+
+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_headline' => '(bool) Show headline in related posts?',
+ 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
+ 'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
+ 'jetpack_search_supported' => '(bool) Jetpack Search supported',
+ 'jetpack_protect_whitelist' => '(array) List of IP addresses to whitelist',
+ '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 follows my blog?',
+ 'comment_moderation' => '(bool) Moderate comments for manual approval?',
+ 'comment_whitelist' => '(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',
+ 'blacklist_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',
+ 'amp_is_enabled' => '(bool) Whether AMP is enabled for this site',
+ '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',
+ ),
+
+ 'response_format' => array(
+ 'updated' => '(array)'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1',
+) );
+
+class WPCOM_JSON_API_Site_Settings_V1_3_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_2_Endpoint {
+ protected function get_defaults() {
+ return array(
+ 'code' => '',
+ 'anonymize_ip' => false,
+ 'ec_track_purchases' => false,
+ 'ec_track_add_to_cart' => false
+ );
+ }
+
+ function callback( $path = '', $blog_id = 0 ) {
+ add_filter( 'site_settings_endpoint_get', array( $this, 'filter_site_settings_endpoint_get' ) );
+ add_filter( 'site_settings_update_wga', array( $this, 'filter_update_google_analytics' ), 10, 2 );
+ return parent::callback( $path, $blog_id );
+ }
+
+ /**
+ * Filter the parent's response to include the fields
+ * added to 1.3 (and their defaults)
+ */
+ public function filter_site_settings_endpoint_get( $settings ) {
+ $option_name = defined( 'IS_WPCOM' ) && IS_WPCOM ? 'wga' : 'jetpack_wga';
+ $option = get_option( $option_name, array() );
+ $settings[ 'wga' ] = wp_parse_args( $option, $this->get_defaults() );
+ return $settings;
+ }
+
+ /**
+ * Filter the parent's response to consume our new fields
+ */
+ public function filter_update_google_analytics( $wga, $new_values ) {
+ $wga_keys = array_keys( $this->get_defaults() );
+ foreach ( $wga_keys as $wga_key ) {
+ // Skip code since the parent class has handled it
+ if ( 'code' === $wga_key ) {
+ continue;
+ }
+ // All our new keys are booleans, so let's coerce each key's value
+ // before updating the value in settings
+ if ( array_key_exists( $wga_key, $new_values ) ) {
+ $wga[ $wga_key ] = WPCOM_JSON_API::is_truthy( $new_values[ $wga_key ] );
+ }
+ }
+ return $wga;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-4-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-4-endpoint.php
new file mode 100644
index 00000000..ab59ebcc
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-v1-4-endpoint.php
@@ -0,0 +1,127 @@
+<?php
+
+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(
+ '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_headline' => '(bool) Show headline in related posts?',
+ 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
+ 'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
+ 'jetpack_search_supported' => '(bool) Jetpack Search supported',
+ 'jetpack_protect_whitelist' => '(array) List of IP addresses to whitelist',
+ '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 follows my blog?',
+ 'comment_moderation' => '(bool) Moderate comments for manual approval?',
+ 'comment_whitelist' => '(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',
+ 'blacklist_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',
+ 'amp_is_enabled' => '(bool) Whether AMP is enabled for this site',
+ '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',
+ ),
+
+ 'response_format' => array(
+ 'updated' => '(array)'
+ ),
+
+ 'example_request' => 'https://public-api.wordpress.com/rest/v1.4/sites/en.blog.wordpress.com/settings?pretty=1',
+) );
+
+class WPCOM_JSON_API_Site_Settings_V1_4_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_3_Endpoint {
+ protected function get_defaults() {
+ return array(
+ 'code' => '',
+ 'anonymize_ip' => 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,
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-user-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-user-endpoint.php
new file mode 100644
index 00000000..b20fa6c2
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-user-endpoint.php
@@ -0,0 +1,221 @@
+<?php
+
+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:\/\/en.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:\/\/en.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:\/\/en.gravatar.com\/binarysmash",
+ "roles": [ "administrator" ]
+ }'
+) );
+
+class WPCOM_JSON_API_Site_User_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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',
+ );
+
+ // /sites/%s/users/%d -> $blog_id, $user_id
+ 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 = false !== strpos( $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 );
+ } else if ( '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.' );
+ }
+ }
+
+ 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();
+ }
+
+ return $the_user;
+ }
+
+ /**
+ * Updates user data
+ *
+ * @return array
+ */
+ public function update_user( $user_id, $blog_id ) {
+ $input = $this->input();
+ $user['ID'] = $user_id;
+ $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
+
+ if ( get_current_user_id() == $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() && $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];
+ } else if ( 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 ) ) {
+ return new WP_Error( 'invalid_input', sprintf( __( '%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 );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-taxonomy-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-taxonomy-endpoint.php
new file mode 100644
index 00000000..312fa195
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-taxonomy-endpoint.php
@@ -0,0 +1,30 @@
+<?php
+abstract class WPCOM_JSON_API_Taxonomy_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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',
+ );
+
+ 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',
+ );
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-comment-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-comment-endpoint.php
new file mode 100644
index 00000000..d60d541f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-comment-endpoint.php
@@ -0,0 +1,393 @@
+<?php
+
+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,
+
+ '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,
+
+ '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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
+ 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.';
+ }
+ }
+
+ // /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
+ 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 ( false !== strpos( $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 );
+ }
+
+ // /sites/%s/posts/%d/replies/new -> $blog_id, $post_id
+ // /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
+ function new_comment( $path, $blog_id, $post_id, $comment_parent_id ) {
+ 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 (
+ -1 == get_option( 'blog_public' ) &&
+ /**
+ * 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;
+ }
+
+ // If we have a user with an external ID saved, we can use it.
+ if (
+ ! $auth_required
+ && $user->ID
+ && (
+ $author = get_user_by( 'id', intval( $user->ID ) )
+ )
+ ) {
+ $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' => '',
+ );
+
+ if ( $comment_parent_id ) {
+ if ( $comment_parent->comment_approved === '0' && 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;
+ }
+
+ // /sites/%s/comments/%d -> $blog_id, $comment_id
+ 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;
+ }
+
+ // /sites/%s/comments/%d/delete -> $blog_id, $comment_id
+ 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'] );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-customcss.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-customcss.php
new file mode 100644
index 00000000..c67ac3e4
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-customcss.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Custom Css update endpoint
+ *
+ * https://public-api.wordpress.com/rest/v1.1/sites/$site/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"
+ }'
+) );
+
+class WPCOM_JSON_API_Update_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint {
+ /**
+ * API callback.
+ */
+ 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 );
+ }
+}
+
+
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-endpoint.php
new file mode 100644
index 00000000..9b6b2536
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-endpoint.php
@@ -0,0 +1,80 @@
+<?php
+
+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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-v1-1-endpoint.php
new file mode 100644
index 00000000..1284cdcb
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-media-v1-1-endpoint.php
@@ -0,0 +1,134 @@
+<?php
+
+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.",
+ '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',
+ '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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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 ( 0 === strpos( $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 );
+ }
+ }
+
+ $insert['ID'] = $media_id;
+ wp_update_post( (object) $insert );
+
+ $item = $this->get_media_item_v1_1( $media_id );
+ return $item;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-endpoint.php
new file mode 100644
index 00000000..80dccac6
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-endpoint.php
@@ -0,0 +1,921 @@
+<?php
+
+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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
+ 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.';
+ }
+ }
+
+ // /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
+ 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 );
+ }
+ }
+
+ // /sites/%s/posts/new -> $blog_id
+ // /sites/%s/posts/%d -> $blog_id, $post_id
+ function write_post( $path, $blog_id, $post_id ) {
+ $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 );
+
+ 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 );
+ }
+
+ // default to post
+ if ( empty( $input['type'] ) )
+ $input['type'] = 'post';
+
+ $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 );
+ }
+ }
+ } else {
+ if ( !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' );
+ }
+ }
+
+ 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'] = date( '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;
+ } else if ( ! 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'];
+ } else {
+ // Tags must be added by name
+ if ( is_int( $term ) ) {
+ $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
+ $insert['post_content'] = $input['content'] = "[gallery size=full columns=1]\n\n" . $input['content'];
+ break;
+ default :
+ // Several images - 3 column gallery
+ $insert['post_content'] = $input['content'] = "[gallery]\n\n" . $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: http://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 );
+ }
+ $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' );
+ }
+ } else {
+ if ( $likes ) {
+ update_post_meta( $post_id, 'switch_like_status', 1 );
+ } else {
+ delete_post_meta( $post_id, 'switch_like_status' );
+ }
+ }
+ } else {
+ if ( 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' );
+ }
+ } else {
+ if ( 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 );
+ }
+ }
+ else {
+ if ( isset( $sharing ) && true === $sharing ) {
+ delete_post_meta( $post_id, 'sharing_disabled' );
+ } else if ( isset( $sharing ) && false == $sharing ) {
+ 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 ( $publicize === false || ( isset( $this->api->token_details['client_id'] ) && 25952 == $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 );
+ }
+ }
+ } else if ( 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( 'twitter', 'facebook') will only publicize to those, ignoring the other available services
+ * Form data: publicize[]=twitter&publicize[]=facebook
+ * EG: array( 'twitter' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two Twitter accounts, and one Facebook connection, of potentially many.
+ * Form data: publicize[twitter]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7
+ * EG: array( 'twitter', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available Twitter accounts, but only 2 of potentially many Facebook connections
+ * Form data: publicize[]=twitter&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 ) && !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 );
+ }
+ } else if ( !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 ) ) {
+ 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 ( ! is_null( $publicize_custom_message ) ) {
+ 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;
+
+ // Custom meta description can only be set on sites that have a business subscription.
+ if ( Jetpack_SEO_Posts::DESCRIPTION_META_KEY == $meta->key && ! 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 )
+ $meta->value = true;
+ if ( 'false' == $meta->value )
+ $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;
+ }
+
+ // /sites/%s/posts/%d/delete -> $blog_id, $post_id
+ 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'] );
+ }
+
+ // /sites/%s/posts/%d/restore -> $blog_id, $post_id
+ 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'] );
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-1-endpoint.php
new file mode 100644
index 00000000..3778f16d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-1-endpoint.php
@@ -0,0 +1,1005 @@
+<?php
+
+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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
+ 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.';
+ }
+ }
+
+ // /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
+ 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 );
+ }
+ }
+
+ // /sites/%s/posts/new -> $blog_id
+ // /sites/%s/posts/%d -> $blog_id, $post_id
+ function write_post( $path, $blog_id, $post_id ) {
+ 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';
+ }
+
+ 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 );
+ }
+
+ // default to post
+ if ( empty( $input['type'] ) )
+ $input['type'] = 'post';
+
+ $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 );
+ }
+ }
+ } else {
+ if ( !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 );
+ }
+
+ // 'future' is an alias for 'publish' for now
+ if ( isset( $input['status'] ) && 'future' === $input['status'] ) {
+ $input['status'] = 'publish';
+ }
+
+ $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.
+ // 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' );
+ }
+ }
+
+ 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'] = date( '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 );
+ } else if ( ! 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 );
+ } else if ( ! 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;
+ } else if ( ! 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
+ if ( is_int( $term ) ) {
+ $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' ) ) ) {
+ $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 = join( ',', 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
+ $insert['post_content'] = $input['content'] = sprintf(
+ "[gallery size=full ids='%s' columns=1]\n\n",
+ $media_id_string
+ ) . $input['content'];
+ break;
+ default :
+ // Several images - 3 column gallery
+ $insert['post_content'] = $input['content'] = sprintf(
+ "[gallery ids='%s']\n\n",
+ $media_id_string
+ ) . $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: http://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 )",
+ $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' );
+ }
+ } else {
+ if ( $likes ) {
+ update_post_meta( $post_id, 'switch_like_status', 1 );
+ } else {
+ delete_post_meta( $post_id, 'switch_like_status' );
+ }
+ }
+ } else {
+ if ( 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' );
+ }
+ } else {
+ if ( 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 );
+ }
+ }
+ else {
+ if ( isset( $sharing ) && true === $sharing ) {
+ delete_post_meta( $post_id, 'sharing_disabled' );
+ } else if ( isset( $sharing ) && false == $sharing ) {
+ 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 ( $publicize === false || ( isset( $this->api->token_details['client_id'] ) && 25952 == $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 );
+ }
+ }
+ } else if ( 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( 'twitter', 'facebook') will only publicize to those, ignoring the other available services
+ * Form data: publicize[]=twitter&publicize[]=facebook
+ * EG: array( 'twitter' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two Twitter accounts, and one Facebook connection, of potentially many.
+ * Form data: publicize[twitter]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7
+ * EG: array( 'twitter', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available Twitter accounts, but only 2 of potentially many Facebook connections
+ * Form data: publicize[]=twitter&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 ) && !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 );
+ }
+ } else if ( !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 ) ) {
+ 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 ( ! is_null( $publicize_custom_message ) ) {
+ 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;
+
+ // Custom meta description can only be set on sites that have a business subscription.
+ if ( Jetpack_SEO_Posts::DESCRIPTION_META_KEY == $meta->key && ! 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 )
+ $meta->value = true;
+ if ( 'false' == $meta->value )
+ $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;
+ }
+
+ // /sites/%s/posts/%d/delete -> $blog_id, $post_id
+ 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'] );
+ }
+
+ // /sites/%s/posts/%d/restore -> $blog_id, $post_id
+ 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'] );
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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' ) );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-2-endpoint.php
new file mode 100644
index 00000000..113766a1
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-post-v1-2-endpoint.php
@@ -0,0 +1,873 @@
+<?php
+
+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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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 publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
+ 'publicize_message' => '(string) Custom message to be publicized 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.",
+ ),
+
+ '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'
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Update_Post_v1_2_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint {
+
+ // /sites/%s/posts/new -> $blog_id
+ // /sites/%s/posts/%d -> $blog_id, $post_id
+ function write_post( $path, $blog_id, $post_id ) {
+ 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 );
+ 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 ( isset( $input['status'] ) && 'future' === $input['status'] ) {
+ $input['status'] = 'publish';
+ }
+
+ 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 );
+ }
+
+ // default to post
+ if ( empty( $input['type'] ) )
+ $input['type'] = 'post';
+
+ $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 );
+ }
+ }
+ } else {
+ if ( !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 );
+ }
+
+ // 'future' is an alias for 'publish' for now
+ if ( isset( $input['status'] ) && 'future' === $input['status'] ) {
+ $input['status'] = 'publish';
+ }
+
+ $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 = 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' );
+ }
+ }
+
+ 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 f
+ 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'] = date( '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 );
+ } else if ( ! 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 );
+ } else if ( ! 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;
+ } else if ( ! 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' ) ) ) {
+ $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 = join( ',', 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
+ $insert['post_content'] = $input['content'] = sprintf(
+ "[gallery size=full ids='%s' columns=1]\n\n",
+ $media_id_string
+ ) . $input['content'];
+ break;
+ default :
+ // Several images - 3 column gallery
+ $insert['post_content'] = $input['content'] = sprintf(
+ "[gallery ids='%s']\n\n",
+ $media_id_string
+ ) . $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: http://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 )",
+ $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' );
+ }
+ } else {
+ if ( $likes ) {
+ update_post_meta( $post_id, 'switch_like_status', 1 );
+ } else {
+ delete_post_meta( $post_id, 'switch_like_status' );
+ }
+ }
+ } else {
+ if ( 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' );
+ }
+ } else {
+ if ( 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 );
+ }
+ }
+ else {
+ if ( isset( $sharing ) && true === $sharing ) {
+ delete_post_meta( $post_id, 'sharing_disabled' );
+ } else if ( isset( $sharing ) && false == $sharing ) {
+ 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 ( $publicize === false || ( isset( $this->api->token_details['client_id'] ) && 25952 == $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 );
+ }
+ }
+ } else if ( 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( 'twitter', 'facebook') will only publicize to those, ignoring the other available services
+ * Form data: publicize[]=twitter&publicize[]=facebook
+ * EG: array( 'twitter' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two Twitter accounts, and one Facebook connection, of potentially many.
+ * Form data: publicize[twitter]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7
+ * EG: array( 'twitter', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available Twitter accounts, but only 2 of potentially many Facebook connections
+ * Form data: publicize[]=twitter&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 ) && !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 );
+ }
+ } else if ( !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 ) ) {
+ 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 ( ! is_null( $publicize_custom_message ) ) {
+ 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;
+
+ // Custom meta description can only be set on sites that have a business subscription.
+ if ( Jetpack_SEO_Posts::DESCRIPTION_META_KEY == $meta->key && ! 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 )
+ $meta->value = true;
+ if ( 'false' == $meta->value )
+ $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 = 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' !== $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;
+ }
+
+ 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' ) );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-homepage-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-homepage-endpoint.php
new file mode 100644
index 00000000..fa8c4cf8
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-homepage-endpoint.php
@@ -0,0 +1,77 @@
+<?php
+
+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}'
+) );
+
+class WPCOM_JSON_API_Update_Site_Homepage_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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();
+ }
+
+ 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,
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-logo-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-logo-endpoint.php
new file mode 100644
index 00000000..2ccca0a9
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-site-logo-endpoint.php
@@ -0,0 +1,96 @@
+<?php
+
+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',
+ ),
+ '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,
+ 'url' => 'https://s.w.org/about/images/logos/codeispoetry-rgb.png',
+ ),
+ ),
+ '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' ),
+ ),
+) );
+
+class WPCOM_JSON_API_Update_Site_Logo_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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'] ) ) {
+ $logo_settings['id'] = intval( $args['id'], 10 );
+ }
+ if ( isset( $args['url'] ) ) {
+ $logo_settings['url'] = $args['url'];
+ }
+ if ( isset( $args['url'] ) || isset( $args['id'] ) ) {
+ update_option( 'site_logo', $logo_settings );
+ }
+
+ return $this->get_current_settings();
+ }
+
+ function get_current_settings() {
+ $logo_settings = get_option( 'site_logo' );
+ if ( ! is_array( $logo_settings ) ) {
+ $logo_settings = array();
+ }
+ return $logo_settings;
+ }
+}
+
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-taxonomy-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-taxonomy-endpoint.php
new file mode 100644
index 00000000..5cab5b3f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-taxonomy-endpoint.php
@@ -0,0 +1,314 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Update_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
+ // /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
+ 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 );
+ }
+
+ // /sites/%s/tags|categories/new -> $blog_id
+ 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;
+
+ if ( $term = get_term_by( 'name', $input['name'], $taxonomy_type ) ) {
+ // 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;
+ }
+
+ // /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id
+ 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;
+ }
+
+ // /sites/%s/tags|categories/%s/delete -> $blog_id, $taxonomy_id
+ 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',
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-term-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-term-endpoint.php
new file mode 100644
index 00000000..ab85c41f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-term-endpoint.php
@@ -0,0 +1,238 @@
+<?php
+
+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'
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Update_Term_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
+ // /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
+ 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 );
+ } else if ( $this->api->ends_with( $path, '/new' ) ) {
+ return $this->new_term( $path, $blog_id, $taxonomy );
+ }
+
+ return $this->update_term( $path, $blog_id, $slug, $taxonomy );
+ }
+
+ // /sites/%s/taxonomies/%s/terms/new -> $blog_id, $taxonomy
+ 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;
+ }
+
+ if ( $term = get_term_by( 'name', $input['name'], $taxonomy ) ) {
+ // 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;
+ }
+
+ // /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug
+ 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;
+ }
+
+ // /sites/%s/taxonomies/%s/terms/slug:%s/delete -> $blog_id, $taxonomy, $slug
+ 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
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-user-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-user-endpoint.php
new file mode 100644
index 00000000..8ec4c59c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-update-user-endpoint.php
@@ -0,0 +1,165 @@
+<?php
+
+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
+ }'
+) );
+
+class WPCOM_JSON_API_Update_User_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ 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 ( wpcom_get_blog_owner( $blog_id ) == $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
+ * @return bool
+ */
+ 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
+ * @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
+ * @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 $domains;
+ }
+
+ /**
+ * Validates user input and then decides whether to remove or delete a user.
+ * @param int $user_id
+ * @return array|WP_Error
+ */
+ function delete_or_remove_user( $user_id ) {
+ if ( 0 == $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 ) ) {
+ return new WP_Error( 'user_owns_domain_subscription', join( ', ', $domains ) );
+ }
+ }
+
+ if ( get_current_user_id() == $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
+ * @return array|WP_Error
+ */
+ 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
+ * @return array|WP_Error
+ */
+ 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 ( $user_id == $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, intval( $input['reassign'] ) ),
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-endpoint.php
new file mode 100644
index 00000000..a47b5d0c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-endpoint.php
@@ -0,0 +1,94 @@
+<?php
+
+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"
+ )
+ )
+) );
+
+class WPCOM_JSON_API_Upload_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
+ 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;
+
+ $media_ids = $files = $errors = 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 ( 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-v1-1-endpoint.php b/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-v1-1-endpoint.php
new file mode 100644
index 00000000..a46d0afd
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/class.wpcom-json-api-upload-media-v1-1-endpoint.php
@@ -0,0 +1,176 @@
+<?php
+
+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/posts/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',
+ ),
+ )
+) );
+
+class WPCOM_JSON_API_Upload_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
+
+ /**
+ * @param string $path
+ * @param int $blog_id
+ *
+ * @return array|int|WP_Error|void
+ */
+ 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.' );
+ }
+
+ $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 ( preg_match( '@^video/@', $media_item['type'] ) && $is_jetpack_site ) {
+ $jetpack_media_files[] = $media_item;
+
+ } else {
+ $other_media_files[] = $media_item;
+ }
+ }
+
+ // New Jetpack / VideoPress media upload processing
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ if ( count( $jetpack_media_files ) > 0 ) {
+ add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
+
+ $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'] );
+ }
+ }
+ }
+
+ // Normal WPCOM upload processing
+ if ( count( $other_media_files ) > 0 || count( $media_urls ) > 0 ) {
+ $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 ( count( $media_items ) <= 0 ) {
+ return $this->api->output_early( 400, array( 'errors' => $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 ( count( $errors ) > 0 ) {
+ $response['errors'] = $errors;
+ }
+
+ return $response;
+ }
+
+ /**
+ * 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
+ * @return bool
+ */
+ 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 ( ! 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-check-capabilities-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-check-capabilities-endpoint.php
new file mode 100644
index 00000000..c86cddec
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-check-capabilities-endpoint.php
@@ -0,0 +1,26 @@
+<?php
+
+class Jetpack_JSON_API_Check_Capabilities_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
+ // GET /sites/%s/me/capability
+ // The unused $object parameter is for making the method signature compatible with its parent class method.
+ public function callback( $path = '', $_blog_id = 0, $object = null ) {
+ // Check minimum capability and blog membership first
+ if ( is_wp_error( $error = $this->validate_call( $_blog_id, 'read', false ) ) ) {
+ 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 );
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-endpoint.php
new file mode 100644
index 00000000..f63a6cd7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-endpoint.php
@@ -0,0 +1,20 @@
+<?php
+
+class Jetpack_JSON_API_Core_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // POST /sites/%s/core
+ // POST /sites/%s/core/update
+ protected $needed_capabilities = 'manage_options';
+ protected $new_version;
+ protected $log;
+
+ 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,
+ );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-modify-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-modify-endpoint.php
new file mode 100644
index 00000000..8f707ad4
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-core-modify-endpoint.php
@@ -0,0 +1,75 @@
+<?php
+
+class Jetpack_JSON_API_Core_Modify_Endpoint extends Jetpack_JSON_API_Core_Endpoint {
+ // POST /sites/%s/core
+ // POST /sites/%s/core/update
+ protected $needed_capabilities = 'update_core';
+ protected $action = 'default_action';
+ protected $new_version;
+ protected $log;
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php
new file mode 100644
index 00000000..9638c3eb
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-cron-endpoint.php
@@ -0,0 +1,252 @@
+<?php
+
+// GET /sites/%s/cron
+class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint {
+ protected $needed_capabilities = 'manage_options';
+
+ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
+ return parent::validate_call( $_blog_id, $capability, false );
+ }
+
+ protected function result() {
+ return array(
+ 'cron_array' => _get_cron_array(),
+ 'current_timestamp' => time()
+ );
+ }
+
+ protected function sanitize_hook( $hook ) {
+ return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook );
+ }
+
+ protected function resolve_arguments() {
+ $args = $this->input();
+ return isset( $args['arguments'] ) ? json_decode( $args['arguments'] ) : array();
+ }
+
+ 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;
+ }
+
+ protected function maybe_unlock_cron( $doing_wp_cron ) {
+ if ( $this->get_cron_lock() == $doing_wp_cron ) {
+ delete_transient( 'doing_cron' );
+ }
+ }
+
+ protected function lock_cron() {
+ $lock = sprintf( '%.22F', microtime( true ) );
+ set_transient( 'doing_cron', $lock );
+ return $lock;
+ }
+
+ protected function get_schedules( $hook, $args ) {
+ $crons = _get_cron_array();
+ $key = md5(serialize($args));
+ 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;
+ }
+}
+
+// POST /sites/%s/cron
+class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {
+
+ protected function result() {
+ define( 'DOING_CRON', true );
+ set_time_limit( 0 );
+ $args = $this->input();
+
+ if ( false === $crons = _get_cron_array() ) {
+ 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'] ) ) {
+ continue;
+ }
+
+ foreach ( $hook_data as $hash => $hook_item ) {
+
+ $schedule = $hook_item['schedule'];
+ $arguments = $hook_item['args'];
+
+ if ( $schedule != false ) {
+ 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 );
+ }
+}
+
+// POST /sites/%s/cron/schedule
+class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {
+
+ 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 ( 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 );
+ /**
+ * Note: Before WP 5.1, the return value was either `false` or `null`.
+ * With 5.1 and later, the return value is now `false` or `true`.
+ * We need to account for both.
+ */
+ return array( 'success' => false !== $next );
+ }
+}
+
+// POST /sites/%s/cron/unschedule
+class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint {
+
+ 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 ) ) {
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-endpoint.php
new file mode 100644
index 00000000..8a349015
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-endpoint.php
@@ -0,0 +1,128 @@
+<?php
+
+include 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 {
+
+ protected $needed_capabilities;
+ protected $expected_actions = array();
+ protected $action;
+
+
+ public function callback( $path = '', $blog_id = 0, $object = null ) {
+ if ( is_wp_error( $error = $this->validate_call( $blog_id, $this->needed_capabilities ) ) ) {
+ return $error;
+ }
+
+ if ( is_wp_error( $error = $this->validate_input( $object ) ) ) {
+ return $error;
+ }
+
+ if ( ! empty( $this->action ) ) {
+ if( is_wp_error( $error = call_user_func( array( $this, $this->action ) ) ) ) {
+ return $error;
+ }
+ }
+
+ return $this->result();
+ }
+
+ abstract protected function result();
+
+ protected function validate_input( $object ) {
+ $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 ) ) {
+ 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.
+ * @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;
+ }
+
+ if ( is_wp_error( $error = $this->check_capability( $capability ) ) ) {
+ 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;
+ }
+
+ /**
+ * @param $capability
+ *
+ * @return bool|WP_Error
+ */
+ protected function check_capability( $capability ) {
+ 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 that must have conditions is less then
+ if ( $passed < $must_pass ) {
+ return new WP_Error( 'unauthorized', sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), implode( ', ', $failed ), 403 ) );
+ }
+
+ } else {
+ if ( !current_user_can( $capability ) ) {
+ return new WP_Error( 'unauthorized', sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), $capability ), 403 );
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-comment-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-comment-backup-endpoint.php
new file mode 100644
index 00000000..0e4bc256
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-comment-backup-endpoint.php
@@ -0,0 +1,52 @@
+<?php
+
+class Jetpack_JSON_API_Get_Comment_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/comments/%d/backup -> $blog_id, $comment_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $comment_id;
+
+ 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 = intval( $comment_id );
+
+ return true;
+ }
+
+ protected function result() {
+ $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(),
+ );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-database-object-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-database-object-backup-endpoint.php
new file mode 100644
index 00000000..b7134730
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-database-object-backup-endpoint.php
@@ -0,0 +1,97 @@
+<?php
+
+class Jetpack_JSON_API_Get_Database_Object_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/database-object/backup -> $blog_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $object_type;
+ protected $object_id;
+
+ // Full list of database objects that can be retrieved via this endpoint.
+ 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',
+ ),
+ );
+
+ function validate_input( $object ) {
+ $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;
+ }
+
+ protected function result() {
+ global $wpdb;
+
+ $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 );
+ $object = $wpdb->get_row( $query );
+
+ 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 );
+ $result[ 'children' ] = $wpdb->get_results( $query );
+ }
+
+ return $result;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-option-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-option-backup-endpoint.php
new file mode 100644
index 00000000..a5d8d3a7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-option-backup-endpoint.php
@@ -0,0 +1,35 @@
+<?php
+
+class Jetpack_JSON_API_Get_Option_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/options/backup -> $blog_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $option_names;
+
+ function validate_input( $object ) {
+ $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;
+ }
+
+ protected function result() {
+ $options = array_map( array( $this, 'get_option_row' ), $this->option_names );
+ return array( 'options' => $options );
+ }
+
+ private function get_option_row( $name ) {
+ global $wpdb;
+ return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-post-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-post-backup-endpoint.php
new file mode 100644
index 00000000..903a16ac
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-post-backup-endpoint.php
@@ -0,0 +1,31 @@
+<?php
+
+class Jetpack_JSON_API_Get_Post_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/posts/%d/backup -> $blog_id, $post_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $post_id;
+
+ 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 = intval( $post_id );
+
+ return true;
+ }
+
+ protected function result() {
+ $post = get_post( $this->post_id );
+ if ( empty( $post ) ) {
+ return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack' ), 404 );
+ }
+
+ return array(
+ 'post' => (array)$post,
+ 'meta' => get_post_meta( $post->ID ),
+ );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-term-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-term-backup-endpoint.php
new file mode 100644
index 00000000..40d0ab97
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-term-backup-endpoint.php
@@ -0,0 +1,32 @@
+<?php
+
+class Jetpack_JSON_API_Get_Term_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/terms/%d/backup -> $blog_id, $term_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $term_id;
+
+ 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 = intval( $term_id );
+
+ return true;
+ }
+
+ protected function result() {
+ $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 ),
+ );
+ }
+
+}
+
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-user-backup-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-user-backup-endpoint.php
new file mode 100644
index 00000000..22ca195d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-get-user-backup-endpoint.php
@@ -0,0 +1,32 @@
+<?php
+
+class Jetpack_JSON_API_Get_User_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // /sites/%s/users/%d/backup -> $blog_id, $user_id
+
+ protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
+ protected $user_id;
+
+ 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 = intval( $user_id );
+
+ return true;
+ }
+
+ protected function result() {
+ $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 ),
+ );
+ }
+
+}
+
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-jps-woocommerce-connect-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-jps-woocommerce-connect-endpoint.php
new file mode 100644
index 00000000..75a3b04d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-jps-woocommerce-connect-endpoint.php
@@ -0,0 +1,58 @@
+<?php
+
+class Jetpack_JSON_API_JPS_WooCommerce_Connect_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $needed_capabilities = 'manage_options';
+
+ 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,
+ );
+ }
+
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-log-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-log-endpoint.php
new file mode 100644
index 00000000..f1d0f4da
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-log-endpoint.php
@@ -0,0 +1,16 @@
+<?php
+
+class Jetpack_JSON_API_Jetpack_Log_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // GET /sites/%s/jetpack-log
+ protected $needed_capabilities = 'manage_options';
+
+ protected function result() {
+ $args = $this->input();
+ $event = ( isset( $args['event'] ) && is_string( $args['event'] ) ) ? $code : false;
+ $num = ( isset( $args['num'] ) ) ? intval( $num ) : false;
+
+ return array(
+ 'log' => Jetpack::get_log( $event, $num )
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-maybe-auto-update-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-maybe-auto-update-endpoint.php
new file mode 100644
index 00000000..5b368760
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-maybe-auto-update-endpoint.php
@@ -0,0 +1,32 @@
+<?php
+
+class Jetpack_JSON_API_Maybe_Auto_Update_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // POST /sites/%s/maybe_auto_update
+ protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' );
+
+ protected $update_results = array();
+
+ protected function result() {
+ add_action( 'automatic_updates_complete', array( $this, 'get_update_results' ), 100, 1 );
+
+ wp_maybe_auto_update();
+
+ $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;
+ }
+
+ public function get_update_results( $results ) {
+ $this->update_results = $results;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-endpoint.php
new file mode 100644
index 00000000..2f56f1ee
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-endpoint.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * Base class for working with Jetpack Modules.
+ */
+abstract class Jetpack_JSON_API_Modules_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $modules = array();
+
+ protected $bulk = true;
+
+ static $_response_format = array(
+ '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\'',
+ );
+
+ 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 $theme
+ *
+ * @return bool
+ */
+ 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;
+ }
+
+ if ( is_wp_error( $error = $this->validate_modules() ) ) {
+ 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 ) ) {
+ return new WP_Error( 'unknown_jetpack_module', sprintf( __( 'Module not found: `%s`.', 'jetpack' ), $module ), 404 );
+ }
+ }
+ return true;
+ }
+
+ 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'] = $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 );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-get-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-get-endpoint.php
new file mode 100644
index 00000000..28a70dba
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-get-endpoint.php
@@ -0,0 +1,6 @@
+<?php
+
+class Jetpack_JSON_API_Modules_Get_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
+ // GET /sites/%s/jetpack/modules/%s
+ protected $needed_capabilities = 'jetpack_manage_modules';
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-list-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-list-endpoint.php
new file mode 100644
index 00000000..2ed4dbdd
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-list-endpoint.php
@@ -0,0 +1,13 @@
+<?php
+
+class Jetpack_JSON_API_Modules_List_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
+ // GET /sites/%s/jetpack/modules
+
+ protected $needed_capabilities = 'jetpack_manage_modules';
+
+ public function validate_input( $module ) {
+ $this->modules = Jetpack::get_available_modules();
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-modify-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-modify-endpoint.php
new file mode 100644
index 00000000..e1562f50
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-modules-modify-endpoint.php
@@ -0,0 +1,62 @@
+<?php
+
+class Jetpack_JSON_API_Modules_Modify_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
+ // POST /sites/%s/jetpack/modules/%s/activate
+ // POST /sites/%s/jetpack/modules/%s
+ // POST /sites/%s/jetpack/modules
+
+ protected $needed_capabilities = 'activate_plugins';
+ protected $action = '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;
+ }
+
+ protected function activate_module() {
+ foreach ( $this->modules as $module ) {
+ if ( Jetpack::is_module_active( $module ) ) {
+ $error = $this->log[ $module ][] = __( 'The Jetpack Module is already activated.', 'jetpack' );
+ continue;
+ }
+ $result = Jetpack::activate_module( $module, false, false );
+ if ( false === $result || ! Jetpack::is_module_active( $module ) ) {
+ $error = $this->log[ $module ][] = __( 'There was an error while activating the module.', 'jetpack' );
+ }
+ }
+
+ if ( ! $this->bulk && isset( $error ) ) {
+ return new WP_Error( 'activation_error', $error, 400 );
+ }
+
+ return true;
+ }
+
+ protected function deactivate_module() {
+ foreach ( $this->modules as $module ) {
+ if ( ! Jetpack::is_module_active( $module ) ) {
+ $error = $this->log[ $module ][] = __( 'The Jetpack Module is already deactivated.', 'jetpack' );
+ continue;
+ }
+ $result = Jetpack::deactivate_module( $module );
+ if ( false === $result || Jetpack::is_module_active( $module ) ) {
+ $error = $this->log[ $module ][] = __( 'There was an error while deactivating the module.', 'jetpack' );
+ }
+ }
+
+ if ( ! $this->bulk && isset( $error ) ) {
+ return new WP_Error( 'deactivation_error', $error, 400 );
+ }
+
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-delete-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-delete-endpoint.php
new file mode 100644
index 00000000..3748d621
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-delete-endpoint.php
@@ -0,0 +1,78 @@
+<?php
+// 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,
+ '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,
+ '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'
+ )
+);
+
+class Jetpack_JSON_API_Plugins_Delete_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+
+ // POST /sites/%s/plugins/%s/delete
+ protected $needed_capabilities = 'delete_plugins';
+ protected $action = 'delete';
+
+ protected function delete() {
+
+ foreach ( $this->plugins as $plugin ) {
+
+ if ( Jetpack::is_plugin_active( $plugin ) ) {
+ $error = $this->log[ $plugin ][] = __( 'You cannot delete a plugin while it is active on the main site.', 'jetpack' );
+ continue;
+ }
+
+ $result = delete_plugins( array( $plugin ) );
+ if ( is_wp_error( $result ) ) {
+ $error = $this->log[ $plugin ][] = $result->get_error_message();
+ } else {
+ $this->log[ $plugin ][] = 'Plugin deleted';
+ }
+ }
+
+ if ( ! $this->bulk && isset( $error ) ) {
+ return new WP_Error( 'delete_plugin_error', $error, 400 );
+ }
+
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-endpoint.php
new file mode 100644
index 00000000..1df4fe66
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-endpoint.php
@@ -0,0 +1,321 @@
+<?php
+
+/**
+ * Base class for working with plugins.
+ */
+abstract class Jetpack_JSON_API_Plugins_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $plugins = array();
+
+ protected $network_wide = false;
+
+ protected $bulk = true;
+ protected $log;
+
+ static $_response_format = array(
+ '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.',
+ );
+
+ static $_response_format_v1_2 = array(
+ '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.',
+ );
+
+ protected function result() {
+
+ $plugins = $this->get_plugins();
+
+ if ( ! $this->bulk && ! empty( $plugins ) ) {
+ return array_pop( $plugins );
+ }
+
+ return array( 'plugins' => $plugins );
+
+ }
+
+ protected function validate_input( $plugin ) {
+
+ if ( is_wp_error( $error = parent::validate_input( $plugin ) ) ) {
+ return $error;
+ }
+
+ if ( is_wp_error( $error = $this->validate_network_wide() ) ) {
+ 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 );
+ }
+
+ if ( is_wp_error( $error = $this->validate_plugins() ) ) {
+ 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;
+ }
+
+ 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'] = date( '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;
+ }
+
+ $autoupdate = in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins', array() ) );
+ $plugin['autoupdate'] = $autoupdate;
+
+ $autoupdate_translation = in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ) );
+ $plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
+
+ $plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file );
+
+ if ( ! empty ( $this->log[ $plugin_file ] ) ) {
+ $plugin['log'] = $this->log[ $plugin_file ];
+ }
+ return $plugin;
+ }
+
+ 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;
+ }
+
+ $autoupdate = $this->plugin_has_autoupdates_enabled( $plugin_file );
+ $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 ( ! empty ( $this->log[ $plugin_file ] ) ) {
+ $plugin['log'] = $this->log[ $plugin_file ];
+ }
+
+ return $plugin;
+ }
+
+ protected function plugin_has_autoupdates_enabled( $plugin_file ) {
+ return (bool) in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins', array() ) );
+ }
+
+ protected function plugin_has_translations_autoupdates_enabled( $plugin_file ) {
+ return (bool) in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ) );
+ }
+
+
+ protected function get_file_mod_capabilities() {
+ $reasons_can_not_autoupdate = array();
+ $reasons_can_not_modify_files = array();
+
+ $has_file_system_write_access = Jetpack_Sync_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 = Jetpack_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 = Jetpack_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;
+ }
+
+ 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;
+ $plugins[] = $this->format_plugin( $plugin, $installed_plugins[ $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;
+ }
+
+ protected function validate_network_wide() {
+ $args = $this->input();
+
+ if ( isset( $args['network_wide'] ) && $args['network_wide'] ) {
+ $this->network_wide = 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;
+ }
+
+
+ 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 );
+ }
+
+ if ( is_wp_error( $error = validate_plugin( $plugin ) ) ) {
+ return new WP_Error( 'unknown_plugin', $error->get_error_messages() , 404 );
+ }
+
+ return true;
+ }
+
+ 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;
+ }
+
+ protected function get_plugin_action_links( $plugin_file ) {
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-functions.php';
+ return Jetpack_Sync_Functions::get_plugins_action_links( $plugin_file );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-get-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-get-endpoint.php
new file mode 100644
index 00000000..276fca49
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-get-endpoint.php
@@ -0,0 +1,28 @@
+<?php
+
+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',
+ ),
+ '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'
+ )
+);
+// no v1.2 version since it is .com only
+class Jetpack_JSON_API_Plugins_Get_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+ // GET /sites/%s/plugins/%s
+ protected $needed_capabilities = 'activate_plugins';
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-install-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-install-endpoint.php
new file mode 100644
index 00000000..bbd6de19
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-install-endpoint.php
@@ -0,0 +1,96 @@
+<?php
+
+include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+include_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,
+ '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,
+ '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'
+ )
+);
+
+class Jetpack_JSON_API_Plugins_Install_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+
+ // POST /sites/%s/plugins/%s/install
+ protected $needed_capabilities = 'install_plugins';
+ protected $action = 'install';
+
+ protected function install() {
+ jetpack_require_lib( 'plugins' );
+ $result = '';
+ foreach ( $this->plugins as $index => $slug ) {
+ $result = Jetpack_Plugins::install_plugin( $slug );
+ if ( is_wp_error( $result ) ) {
+ $this->log[ $slug ][] = $result->get_error_message();
+ if ( ! $this->bulk ) {
+ return $result;
+ }
+ }
+ }
+
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ // No errors, install worked. Now replace the slug with the actual plugin id
+ $this->plugins[$index] = Jetpack_Plugins::get_plugin_id_by_slug( $slug );
+
+ return true;
+ }
+
+ protected function validate_plugins() {
+ if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) {
+ return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) );
+ }
+
+ jetpack_require_lib( 'plugins' );
+ foreach ( $this->plugins as $index => $slug ) {
+ // make sure it is not already installed
+ if ( Jetpack_Plugins::get_plugin_id_by_slug( $slug ) ) {
+ return new WP_Error( 'plugin_already_installed', __( 'The plugin is already installed', 'jetpack' ) );
+ }
+
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-list-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-list-endpoint.php
new file mode 100644
index 00000000..ff8004d0
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-list-endpoint.php
@@ -0,0 +1,36 @@
+<?php
+
+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'
+ ),
+ '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'
+ )
+);
+// No v1.2 versions since they are .com only
+class Jetpack_JSON_API_Plugins_List_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+ // GET /sites/%s/plugins
+ protected $needed_capabilities = 'activate_plugins';
+ public function validate_input( $plugin ) {
+ wp_update_plugins();
+ $this->plugins = array_keys( get_plugins() );
+ return true;
+ }
+}
+
+
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-endpoint.php
new file mode 100644
index 00000000..49cf43dc
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-endpoint.php
@@ -0,0 +1,420 @@
+<?php
+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',
+ ),
+ '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,
+ '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',
+ ),
+ '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',
+ ),
+ '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'
+ )
+);
+
+class Jetpack_JSON_API_Plugins_Modify_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+ // POST /sites/%s/plugins/%s
+ // POST /sites/%s/plugins
+ protected $slug = null;
+ protected $needed_capabilities = 'activate_plugins';
+ protected $action = 'default_action';
+ protected $expected_actions = array( 'update', 'install', 'delete', 'update_translations' );
+
+ 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';
+ case 'update_translations':
+ case 'update' :
+ $this->needed_capabilities = 'update_plugins';
+ break;
+ case 'install' :
+ $this->needed_capabilities = 'install_plugins';
+ break;
+ }
+
+ if ( isset( $args['autoupdate'] ) || isset( $args['autoupdate_translations'] ) ) {
+ $this->needed_capabilities = 'update_plugins';
+ }
+
+ return parent::callback( $path, $blog_id, $object );
+ }
+
+ 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();
+ } else {
+ if ( $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;
+ }
+
+ protected function autoupdate_on() {
+ $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins', array() );
+ $autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) );
+ Jetpack_Options::update_option( 'autoupdate_plugins', $autoupdate_plugins );
+ }
+
+ protected function autoupdate_off() {
+ $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins', array() );
+ $autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins );
+ Jetpack_Options::update_option( 'autoupdate_plugins', $autoupdate_plugins );
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+ 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.' );
+ $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'] );
+ }
+ }
+
+ protected function current_user_can( $capability, $plugin = null ) {
+ if ( $plugin ) {
+ return current_user_can( $capability, $plugin );
+ }
+
+ return current_user_can( $capability );
+ }
+
+ protected function deactivate() {
+ $permission_error = false;
+ foreach ( $this->plugins as $plugin ) {
+ if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) {
+ $error = $this->log[$plugin]['error'] = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' );
+ $permission_error = true;
+ continue;
+ }
+
+ if ( ! Jetpack::is_plugin_active( $plugin ) ) {
+ $error = $this->log[$plugin]['error'] = __( 'The Plugin is already deactivated.', 'jetpack' );
+ 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 = $this->log[$plugin]['error'] = __( 'There was an error deactivating your plugin', 'jetpack' );
+ 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 );
+ }
+ }
+
+ protected function update() {
+ $query_args = $this->query_args();
+ if ( isset( $query_args['autoupdate'] ) && $query_args['autoupdate'] ) {
+ Jetpack_Constants::set_constant( 'JETPACK_PLUGIN_AUTOUPDATE', true );
+ }
+ wp_clean_plugins_cache();
+ 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' );
+
+ $result = false;
+
+ foreach ( $this->plugins as $plugin ) {
+
+ if ( ! in_array( $plugin, $plugin_updates_needed ) ) {
+ $this->log[$plugin][] = __( 'No update needed', 'jetpack' );
+ 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 = $upgrader->skin->get_errors();
+ $this->log[$plugin] = $upgrader->skin->get_upgrade_messages();
+
+ if ( is_wp_error( $errors ) && $errors->get_error_code() ) {
+ return $errors;
+ }
+ }
+
+ 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();
+ }
+
+ function update_translations() {
+ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+
+ // Clear the cache.
+ wp_clean_plugins_cache();
+ 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;
+ }
+
+ protected function get_translation( $translation ) {
+ return ( $translation['slug'] === $this->slug );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-v1-2-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-v1-2-endpoint.php
new file mode 100644
index 00000000..23940da0
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-modify-v1-2-endpoint.php
@@ -0,0 +1,191 @@
+<?php
+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,
+ '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.',
+ ),
+ '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,
+ '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'
+ )
+);
+
+class Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint extends Jetpack_JSON_API_Plugins_Modify_Endpoint {
+
+ protected function activate() {
+ $permission_error = false;
+ $has_errors = 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.' );
+ $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'] );
+ }
+ }
+
+
+ protected function deactivate() {
+ $permission_error = false;
+ foreach ( $this->plugins as $plugin ) {
+ if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) {
+ $error = $this->log[$plugin]['error'] = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' );
+ $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 = $this->log[$plugin]['error'] = __( 'There was an error deactivating your plugin', 'jetpack' );
+ 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 );
+ }
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-new-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-new-endpoint.php
new file mode 100644
index 00000000..286a625b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-plugins-new-endpoint.php
@@ -0,0 +1,136 @@
+<?php
+
+include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+include_once ABSPATH . 'wp-admin/includes/file.php';
+
+
+// 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' => '(zip) Plugin package zip file. multipart/form-data encoded. ',
+ ),
+ '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/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' => '(zip) Plugin package zip file. multipart/form-data encoded. ',
+ ),
+ 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
+ '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'
+ )
+);
+
+class Jetpack_JSON_API_Plugins_New_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
+
+ // POST /sites/%s/plugins/new
+ protected $needed_capabilities = 'install_plugins';
+ protected $action = 'install';
+
+ 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.
+ protected function validate_input( $plugin ) {
+ $this->bulk = false;
+ $this->plugins = array();
+ }
+
+ 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' );
+ }
+ jetpack_require_lib( 'class.jetpack-automatic-install-skin' );
+ $skin = new Jetpack_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 = $upgrader->skin->get_main_error_code();
+ $message = $upgrader->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' );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-sync-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-sync-endpoint.php
new file mode 100644
index 00000000..72dcd52c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-sync-endpoint.php
@@ -0,0 +1,322 @@
+<?php
+
+// POST /sites/%s/sync
+class Jetpack_JSON_API_Sync_Endpoint extends Jetpack_JSON_API_Endpoint {
+ protected $needed_capabilities = 'manage_options';
+
+ protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
+ return parent::validate_call( $_blog_id, $capability, false );
+ }
+
+ 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 ( count( $ids ) > 0 ) {
+ $modules[ $module_name ] = $ids;
+ }
+ }
+ }
+
+ if ( empty( $modules ) ) {
+ $modules = null;
+ }
+ return array( 'scheduled' => Jetpack_Sync_Actions::do_full_sync( $modules ) );
+ }
+
+ 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' ) ) ) {
+ return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
+ }
+ return $query;
+ }
+}
+
+// GET /sites/%s/sync/status
+class Jetpack_JSON_API_Sync_Status_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ return Jetpack_Sync_Actions::get_sync_status();
+ }
+}
+
+// GET /sites/%s/data-check
+class Jetpack_JSON_API_Sync_Check_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-wp-replicastore.php';
+ $store = new Jetpack_Sync_WP_Replicastore();
+ return $store->checksum_all();
+ }
+}
+
+// GET /sites/%s/data-histogram
+class Jetpack_JSON_API_Sync_Histogram_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ $args = $this->query_args();
+
+ if ( isset( $args['columns'] ) ) {
+ $columns = array_map( 'trim', explode( ',', $args['columns'] ) );
+ } else {
+ $columns = null; // go with defaults
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-wp-replicastore.php';
+ $store = new Jetpack_Sync_WP_Replicastore();
+
+ if ( ! isset( $args['strip_non_ascii'] ) ) {
+ $args['strip_non_ascii'] = true;
+ }
+ $histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $columns, $args['strip_non_ascii'], $args['shared_salt'] );
+
+ return array( 'histogram' => $histogram, 'type' => $store->get_checksum_type() );
+ }
+}
+
+// POST /sites/%s/sync/settings
+class Jetpack_JSON_API_Sync_Modify_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ $args = $this->input();
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-settings.php';
+
+ $sync_settings = Jetpack_Sync_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;
+ }
+ }
+
+ Jetpack_Sync_Settings::update_settings( $sync_settings );
+
+ // re-fetch so we see what's really being stored
+ return Jetpack_Sync_Settings::get_settings();
+ }
+}
+
+// GET /sites/%s/sync/settings
+class Jetpack_JSON_API_Sync_Get_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-settings.php';
+
+ return Jetpack_Sync_Settings::get_settings();
+ }
+}
+
+// GET /sites/%s/sync/object
+class Jetpack_JSON_API_Sync_Object extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ $args = $this->query_args();
+
+ $module_name = $args['module_name'];
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-modules.php';
+
+ if ( ! $sync_module = Jetpack_Sync_Modules::get_module( $module_name ) ) {
+ return new WP_Error( 'invalid_module', 'You specified an invalid sync module' );
+ }
+
+ $object_type = $args['object_type'];
+ $object_ids = $args['object_ids'];
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-sender.php';
+ $codec = Jetpack_Sync_Sender::get_instance()->get_codec();
+
+ Jetpack_Sync_Settings::set_is_syncing( true );
+ $objects = $codec->encode( $sync_module->get_objects_by_id( $object_type, $object_ids ) );
+ Jetpack_Sync_Settings::set_is_syncing( false );
+
+ return array(
+ 'objects' => $objects,
+ 'codec' => $codec->name(),
+ );
+ }
+}
+
+class Jetpack_JSON_API_Sync_Now_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ $args = $this->input();
+ $queue_name = $this->validate_queue( $args['queue'] );
+
+ if ( is_wp_error( $queue_name ) ){
+ return $queue_name;
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-sender.php';
+
+ $sender = Jetpack_Sync_Sender::get_instance();
+ $response = $sender->do_sync_for_queue( new Jetpack_Sync_Queue( $args['queue'] ) );
+
+ return array(
+ 'response' => $response
+ );
+ }
+}
+
+class Jetpack_JSON_API_Sync_Checkout_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ 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 );
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-queue.php';
+ $queue = new Jetpack_Sync_Queue( $queue_name );
+
+ if ( 0 === $queue->size() ) {
+ return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-sender.php';
+ $sender = Jetpack_Sync_Sender::get_instance();
+
+ // try to give ourselves as much time as possible
+ set_time_limit( 0 );
+
+ // let's delete the checkin state
+ if ( $args['force'] ) {
+ $queue->unlock();
+ }
+
+ $buffer = $this->get_buffer( $queue, $args[ '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 );
+ }
+
+ Jetpack_Sync_Settings::set_is_syncing( true );
+ list( $items_to_send, $skipped_items_ids, $items ) = $sender->get_items_to_send( $buffer, $args['encode'] );
+ Jetpack_Sync_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(),
+ );
+ }
+
+ 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;
+ }
+}
+
+class Jetpack_JSON_API_Sync_Close_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ protected function result() {
+ $request_body = $this->input();
+ $queue_name = $this->validate_queue( $request_body['queue'] );
+
+ if ( is_wp_error( $queue_name ) ) {
+ return $queue_name;
+ }
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-queue.php';
+
+ 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 );
+ }
+
+ //Limit to A-Z,a-z,0-9,_,-
+ $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'] ) );
+
+ $buffer = new Jetpack_Sync_Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] );
+ $queue = new Jetpack_Sync_Queue( $queue_name );
+
+ $response = $queue->close( $buffer, $request_body['item_ids'] );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ return array(
+ 'success' => $response
+ );
+ }
+
+ protected static function sanitize_item_ids( $item ) {
+ // lets not delete any options that don't start with jpsq_sync-
+ if ( substr( $item, 0, 5 ) !== 'jpsq_' ) {
+ return null;
+ }
+ //Limit to A-Z,a-z,0-9,_,-,.
+ return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item );
+ }
+}
+
+class Jetpack_JSON_API_Sync_Unlock_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
+ 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' ) ) ) {
+ return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-queue.php';
+ $queue = new Jetpack_Sync_Queue( $args['queue'] );
+
+ // False means that there was no lock to delete.
+ $response = $queue->unlock();
+ return array(
+ 'success' => $response
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-active-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-active-endpoint.php
new file mode 100644
index 00000000..db23c52f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-active-endpoint.php
@@ -0,0 +1,50 @@
+<?php
+
+class Jetpack_JSON_API_Themes_Active_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+ // GET /sites/%s/themes/mine => current theme
+ // POST /sites/%s/themes/mine => switch theme
+ // The unused $object parameter is for making the method signature compatible with its parent class method.
+ public function callback( $path = '', $blog_id = 0, $object = null ) {
+
+ if ( is_wp_error( $error = $this->validate_call( $blog_id, 'switch_themes', true ) ) ) {
+ return $error;
+ }
+
+ if ( 'POST' === $this->api->method )
+ return $this->switch_theme();
+ else
+ return $this->get_current_theme();
+ }
+
+ 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 );
+ }
+
+ $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();
+ }
+
+ protected function get_current_theme() {
+ return $this->format_theme( wp_get_theme() );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-delete-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-delete-endpoint.php
new file mode 100644
index 00000000..97bcc58d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-delete-endpoint.php
@@ -0,0 +1,60 @@
+<?php
+
+class Jetpack_JSON_API_Themes_Delete_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+
+ // POST /sites/%s/plugins/%s/delete
+ protected $needed_capabilities = 'delete_themes';
+ protected $action = 'delete';
+
+ protected function delete() {
+
+ foreach( $this->themes as $theme ) {
+
+ // Don't delete an active child theme
+ if ( is_child_theme() && $theme == get_stylesheet() ) {
+ $error = $this->log[ $theme ]['error'] = 'You cannot delete a theme while it is active on the main site.';
+ continue;
+ }
+
+ if( $theme == get_template() ) {
+ $error = $this->log[ $theme ]['error'] = 'You cannot delete a theme while it is active on the main site.';
+ 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 = $this->log[ $theme ]['error'] = $result->get_error_messages();
+ } else {
+ $this->log[ $theme ][] = 'Theme deleted';
+ }
+ }
+
+ if( ! $this->bulk && isset( $error ) ) {
+ return new WP_Error( 'delete_theme_error', $error, 400 );
+ }
+
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-endpoint.php
new file mode 100644
index 00000000..2e32dccd
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-endpoint.php
@@ -0,0 +1,178 @@
+<?php
+
+
+// THEMES
+
+/**
+ * Base class for working with themes, has useful helper functions.
+ */
+abstract class Jetpack_JSON_API_Themes_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $themes = array();
+
+ protected $bulk = true;
+ protected $log;
+ protected $current_theme_id;
+
+ static $_response_format = array(
+ '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',
+ 'autoupdate' => '(bool) Whether the theme is automatically updated',
+ 'autoupdate_translation' => '(bool) Whether the theme is automatically updating translations',
+ );
+
+ 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 $theme
+ *
+ * @return bool
+ */
+ 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;
+ }
+
+ if ( is_wp_error( $error = $this->validate_themes() ) ) {
+ 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 ) {
+ if ( is_wp_error( $error = wp_get_theme( $theme )->errors() ) ) {
+ 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' => jetpack_photon_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() ) );
+ $formatted_theme['autoupdate'] = $autoupdate;
+
+ $autoupdate_translation = in_array( $id, Jetpack_Options::get_option( 'autoupdate_themes_translations', array() ) );
+ $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 );
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-get-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-get-endpoint.php
new file mode 100644
index 00000000..cfc352af
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-get-endpoint.php
@@ -0,0 +1,6 @@
+<?php
+
+class Jetpack_JSON_API_Themes_Get_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+ // GET /sites/%s/themes/%s
+ protected $needed_capabilities = 'switch_themes';
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-install-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-install-endpoint.php
new file mode 100644
index 00000000..c3cec3d3
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-install-endpoint.php
@@ -0,0 +1,173 @@
+<?php
+
+include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+include_once ABSPATH . 'wp-admin/includes/file.php';
+
+class Jetpack_JSON_API_Themes_Install_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+
+ // POST /sites/%s/themes/%s/install
+ protected $needed_capabilities = 'install_themes';
+ protected $action = 'install';
+ protected $download_links = array();
+
+ 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 ) {
+ jetpack_require_lib( 'class.jetpack-automatic-install-skin' );
+ $skin = new Jetpack_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
+ unlink( $link );
+ }
+
+ if ( ! $this->bulk && is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ if ( ! $result ) {
+ $error = $this->log[ $theme ]['error'] = __( 'An unknown error occurred during installation', 'jetpack' );
+ }
+
+ elseif ( ! self::is_installed_theme( $theme ) ) {
+ $error = $this->log[ $theme ]['error'] = __( 'There was an error installing your theme', 'jetpack' );
+ }
+
+ 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;
+ }
+
+ 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 $index => $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/';
+ $args = array(
+ 'body' => array(
+ 'action' => 'theme_information',
+ 'request' => serialize( $params ),
+ )
+ );
+ $response = wp_remote_post( $url, $args );
+ $theme_data = unserialize( $response['body'] );
+ 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;
+ }
+
+ protected static function is_installed_theme( $theme ) {
+ $wp_theme = wp_get_theme( $theme );
+ return $wp_theme->exists();
+ }
+
+ protected static function download_wpcom_theme_to_file( $theme ) {
+ $wpcom_theme_slug = preg_replace( '/-wpcom$/', '', $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 = Jetpack_Client::wpcom_json_api_request_as_blog( $url, '1.1', $args );
+
+ $response = $result[ 'response' ];
+ if ( $response[ 'code' ] !== 200 ) {
+ unlink( $file );
+ return new WP_Error( 'problem_fetching_theme', __( 'Problem downloading theme', 'jetpack' ) );
+ }
+
+ return $file;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-list-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-list-endpoint.php
new file mode 100644
index 00000000..526cf4d7
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-list-endpoint.php
@@ -0,0 +1,13 @@
+<?php
+
+class Jetpack_JSON_API_Themes_List_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+ // GET /sites/%s/themes
+
+ protected $needed_capabilities = 'switch_themes';
+
+ public function validate_input( $theme ) {
+ $this->themes = wp_get_themes( array( 'allowed' => true ) );
+ return true;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-modify-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-modify-endpoint.php
new file mode 100644
index 00000000..072bfc5c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-modify-endpoint.php
@@ -0,0 +1,130 @@
+<?php
+
+class Jetpack_JSON_API_Themes_Modify_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+ // POST /sites/%s/themes/%s
+ // POST /sites/%s/themes
+
+ protected $needed_capabilities = 'update_themes';
+ protected $action = 'default_action';
+ protected $expected_actions = array( 'update', 'update_translations' );
+
+ 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;
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+ 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 );
+ }
+
+ function update() {
+ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+
+ // Clear the cache.
+ wp_update_themes();
+
+ 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;
+ }
+
+ 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' );
+ }
+
+ foreach( $available_themes_updates->translations as $translation ) {
+ $theme = $translation['slug'] ;
+ if ( ! in_array( $translation['slug'], $this->themes ) ) {
+ $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;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-new-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-new-endpoint.php
new file mode 100644
index 00000000..75768183
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-themes-new-endpoint.php
@@ -0,0 +1,83 @@
+<?php
+
+include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+include_once ABSPATH . 'wp-admin/includes/file.php';
+
+class Jetpack_JSON_API_Themes_New_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
+
+ // POST /sites/%s/themes/%s/install
+ protected $needed_capabilities = 'install_themes';
+ protected $action = 'install';
+ protected $download_links = array();
+
+ 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;
+ }
+
+ protected function validate_input( $theme ) {
+ $this->bulk = false;
+ $this->themes = array();
+ }
+
+ 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' );
+ }
+ jetpack_require_lib( 'class.jetpack-automatic-install-skin' );
+ $skin = new Jetpack_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 = $upgrader->skin->get_main_error_code();
+ $message = $upgrader->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' );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-endpoint.php
new file mode 100644
index 00000000..65b6e725
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-endpoint.php
@@ -0,0 +1,20 @@
+<?php
+
+// Translations
+class Jetpack_JSON_API_Translations_Endpoint extends Jetpack_JSON_API_Endpoint {
+ // GET /sites/%s/translations
+ // POST /sites/%s/translations
+ // POST /sites/%s/translations/update
+ protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' );
+ protected $log;
+ protected $success;
+
+ public function result() {
+ return array(
+ 'translations' => wp_get_translation_updates(),
+ 'autoupdate' => Jetpack_Options::get_option( 'autoupdate_translations', false ),
+ 'log' => $this->log,
+ 'success' => $this->success,
+ );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-modify-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-modify-endpoint.php
new file mode 100644
index 00000000..fd5f6a56
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-translations-modify-endpoint.php
@@ -0,0 +1,29 @@
+<?php
+
+class Jetpack_JSON_API_Translations_Modify_Endpoint extends Jetpack_JSON_API_Translations_Endpoint {
+ // POST /sites/%s/translations
+ // POST /sites/%s/translations/update
+ protected $action = 'default_action';
+ protected $new_version;
+ protected $log;
+
+ 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;
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-updates-status-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-updates-status-endpoint.php
new file mode 100644
index 00000000..48f9ae9d
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-updates-status-endpoint.php
@@ -0,0 +1,34 @@
+<?php
+
+class Jetpack_JSON_API_Updates_Status extends Jetpack_JSON_API_Endpoint {
+ // GET /sites/%s/updates
+ protected $needed_capabilities = 'manage_options';
+
+ 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;
+
+ 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;
+
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-connect-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-connect-endpoint.php
new file mode 100644
index 00000000..b30597de
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-connect-endpoint.php
@@ -0,0 +1,30 @@
+<?php
+
+class Jetpack_JSON_API_User_Connect_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $needed_capabilities = 'create_users';
+
+ private $user_id;
+ private $user_token;
+
+ function result() {
+ Jetpack::update_user_token( $this->user_id, sprintf( '%s.%d', $this->user_token, $this->user_id ), false );
+ return array( 'success' => Jetpack::is_user_connected( $this->user_id ) );
+ }
+
+ 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 ( 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-create-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-create-endpoint.php
new file mode 100644
index 00000000..bd71249b
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.jetpack-json-api-user-create-endpoint.php
@@ -0,0 +1,72 @@
+<?php
+
+class Jetpack_JSON_API_User_Create_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $needed_capabilities = 'create_users';
+
+ private $user_data;
+
+ function result() {
+ return $this->create_or_get_user();
+ }
+
+ 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 );
+ }
+
+ function create_or_get_user() {
+ require_once JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php';
+ // 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'] ) {
+ Jetpack_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 = Jetpack_SSO_Helpers::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 );
+ }
+
+ 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;
+ }
+
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php
new file mode 100644
index 00000000..3a76256f
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php
@@ -0,0 +1,41 @@
+<?php
+
+class WPCOM_JSON_API_Get_Option_Endpoint extends Jetpack_JSON_API_Endpoint {
+
+ protected $needed_capabilities = 'manage_options';
+
+ public $option_name;
+ public $site_option;
+
+ 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 ) );
+ }
+
+ function validate_input( $object ) {
+ $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;
+
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-defaults.php';
+ /**
+ * 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', Jetpack_Sync_Defaults::$default_options_whitelist, $this->site_option ) ) ) {
+ return new WP_Error( 'option_name_not_in_whitelist', __( 'You must specify a whitelisted option_name', 'jetpack' ) );
+ }
+ return true;
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-update-option-endpoint.php b/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-update-option-endpoint.php
new file mode 100644
index 00000000..20f8895c
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/class.wpcom-json-api-update-option-endpoint.php
@@ -0,0 +1,31 @@
+<?php
+
+class WPCOM_JSON_API_Update_Option_Endpoint extends WPCOM_JSON_API_Get_Option_Endpoint {
+ public $option_value;
+
+ 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();
+ }
+
+ 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 );
+ }
+}
diff --git a/plugins/jetpack/json-endpoints/jetpack/json-api-jetpack-endpoints.php b/plugins/jetpack/json-endpoints/jetpack/json-api-jetpack-endpoints.php
new file mode 100644
index 00000000..a7fd2acd
--- /dev/null
+++ b/plugins/jetpack/json-endpoints/jetpack/json-api-jetpack-endpoints.php
@@ -0,0 +1,1234 @@
+<?php
+
+$json_jetpack_endpoints_dir = dirname( __FILE__ ) . '/';
+
+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,
+ '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'
+ ),
+ 'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format,
+ '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.',
+ ),
+ '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' => '(zip) Theme package zip file. multipart/form-data encoded. ',
+ ),
+ 'response_format' => Jetpack_JSON_API_Themes_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/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,
+ '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,
+ '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',
+ ),
+ '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,
+ '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,
+ '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,
+ '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,
+ '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',
+ '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.',
+ ),
+ '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-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',
+ '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',
+ 'path_labels' => array(
+ '$site' => '(int|string) The site ID, The site domain'
+ ),
+ 'response_format' => array(
+ '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 sent 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.',
+ ),
+ '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',
+ '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',
+ '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',
+ ),
+ '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'
+) );
+
+$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.',
+ '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',
+ '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',
+);
+
+// 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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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'
+) );
+
+// 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',
+ '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',
+ ),
+ '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',
+ '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.',
+ ),
+ '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',
+ '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',
+ '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',
+ ),
+ '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:\/\/en.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'
+) );