This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,147 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Activate a widget on a site.
*
* Endpoint: https://public-api.wordpress.com/rest/v1.1/sites/$site/widgets/new
*/
new WPCOM_JSON_API_Add_Widgets_Endpoint(
array(
'description' => 'Activate a group of widgets on a site. The bulk version of using the /new endpoint',
'group' => '__do_not_document',
'stat' => 'widgets:new:bulk',
'force' => 'wpcom',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/widgets',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'request_format' => array(
'widgets' => '(array:widget) An array of widget objects to add.',
),
'response_format' => array(
'widgets' => '(array:widget) An array of widget objects added.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/widgets',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'id_base' => 'text',
'sidebar' => 'sidebar-2',
'position' => '0',
'settings' => array( 'title' => 'hello world' ),
),
),
'example_response' => '
{
"id": "text-3",
"id_base": "text",
"settings": {
"title": "hello world"
},
"sidebar": "sidebar-2",
"position": 0
}',
)
);
new WPCOM_JSON_API_Add_Widgets_Endpoint(
array(
'description' => 'Activate a widget on a site.',
'group' => 'sites',
'stat' => 'widgets:new',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/widgets/new',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'request_format' => array(
'id_base' => '(string) The base ID of the widget.',
'sidebar' => '(string) Optional. The ID of the sidebar where this widget will be active. If empty, the widget will be added in the first sidebar available.',
'position' => '(int) Optional. The position of the widget in the sidebar.',
'settings' => '(object) Optional. The settings for the new widget.',
),
'response_format' => array(
'id' => '(string) The actual ID of the widget.',
'sidebar' => '(string) The ID of the sidebar where this widget will be active.',
'position' => '(int) The final position of the widget in the sidebar.',
'settings' => '(array) The settings for the new widget.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/widgets/new',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'id_base' => 'text',
'sidebar' => 'sidebar-2',
'position' => '0',
'settings' => array( 'title' => 'hello world' ),
),
),
'example_response' => '
{
"id": "text-3",
"id_base": "text",
"settings": {
"title": "hello world"
},
"sidebar": "sidebar-2",
"position": 0
}',
)
);
/**
* The Add Widgets endpoint class.
*/
class WPCOM_JSON_API_Add_Widgets_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @uses Jetpack_Widgets
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
// Switch to the given blog.
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error( 'unauthorized', 'User is not authorized to access widgets', 403 );
}
require_once JETPACK__PLUGIN_DIR . '_inc/lib/widgets.php';
$args = $this->input( false, false ); // Don't filter the input.
if ( empty( $args ) || ! is_array( $args ) ) {
return new WP_Error( 'no_data', 'No data was provided.', 400 );
}
if ( isset( $args['widgets'] ) || ! empty( $args['widgets'] ) ) {
$widgets = Jetpack_Widgets::activate_widgets( $args['widgets'] );
if ( is_wp_error( $widgets ) ) {
return $widgets;
}
return array( 'widgets' => $widgets );
}
if ( ! isset( $args['id_base'] ) ) {
return new WP_Error( 'missing_data', 'The data you provided was not accurate.', 400 );
}
if ( empty( $args['sidebar'] ) ) {
$active_sidebars = Jetpack_Widgets::get_active_sidebars();
reset( $active_sidebars );
$args['sidebar'] = key( $active_sidebars );
}
return Jetpack_Widgets::activate_widget( $args['id_base'], $args['sidebar'], $args['position'], $args['settings'] );
}
}
@@ -0,0 +1,135 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint
*
* @package automattic/jetpack
*/
new WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint(
array(
'description' => 'Create a post autosave.',
'group' => '__do_not_document',
'stat' => 'posts:autosave',
'min_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/posts/%d/autosave',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'request_format' => array(
'content' => '(HTML) The post content.',
'title' => '(HTML) The post title.',
'excerpt' => '(HTML) The post excerpt.',
),
'response_format' => array(
'ID' => '(int) autodraft post ID',
'post_ID' => '(int) post ID',
'preview_URL' => '(string) preview URL for the post',
'modified' => '(ISO 8601 datetime) modified time',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/1/autosave',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'title' => 'Howdy',
'content' => 'Hello. I am a test post. I was created by the API',
),
),
)
);
// phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
/**
* Class WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint
*/
class WPCOM_JSON_API_Autosave_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
/**
* Autosave Post callback.
* /sites/%s/posts/%d/autosave -> $blog_id, $post_id
*
* @param string $path Path.
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
*/
public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
if ( ! defined( 'DOING_AUTOSAVE' ) ) {
define( 'DOING_AUTOSAVE', true );
}
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$input = $this->input( false );
if ( ! is_array( $input ) || ! $input ) {
return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// Make sure Custom Post Types, etc. get registered.
$this->load_theme_functions();
}
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
if ( ! current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
}
$post_data = array(
'post_ID' => $post_id,
'post_type' => $post->post_type,
'post_title' => $input['title'],
'post_content' => $input['content'],
'post_excerpt' => $input['excerpt'],
);
$preview_url = add_query_arg( 'preview', 'true', get_permalink( $post->ID ) );
if ( ! wp_check_post_lock( $post->ID ) &&
get_current_user_id() === (int) $post->post_author &&
( 'auto-draft' === $post->post_status || 'draft' === $post->post_status )
) {
// Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked.
$auto_id = edit_post( wp_slash( $post_data ) );
} else {
// Non drafts or other users drafts are not overwritten. The autosave is stored in a special post revision for each user.
$auto_id = wp_create_post_autosave( wp_slash( $post_data ) );
$nonce = wp_create_nonce( 'post_preview_' . $post->ID );
$preview_url = add_query_arg(
array(
'preview_id' => $post->ID,
'preview_nonce' => $nonce,
),
$preview_url
);
}
$updated_post = get_post( $auto_id );
if ( $updated_post && $updated_post->ID && $updated_post->post_modified ) {
return array(
'ID' => $auto_id,
'post_ID' => $post->ID,
'modified' => $this->format_date( $updated_post->post_modified_gmt, $updated_post->post_modified ),
'preview_URL' => $preview_url,
);
} else {
return new WP_Error( 'autosave_error', __( 'Autosave encountered an unexpected error', 'jetpack' ), 500 );
}
}
}
@@ -0,0 +1,85 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Bulk delete posts on a site.
*
* Endpoint: /sites/%s/posts/delete
*/
new WPCOM_JSON_API_Bulk_Delete_Post_Endpoint(
array(
'description' => 'Delete multiple posts. Note: If the trash is enabled, this request will send non-trashed posts to the trash. Trashed posts will be permanently deleted.',
'group' => 'posts',
'stat' => 'posts:1:bulk-delete',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/posts/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'post_ids' => '(array|string) An array, or comma-separated list, of Post IDs to delete or trash.',
),
'response_format' => array(
'results' => '(object) An object containing results, ',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'post_ids' => array( 881, 882 ),
),
),
)
);
/**
* Bulk delete post endpoint class.
*/
class WPCOM_JSON_API_Bulk_Delete_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $post_id - the post ID.
*/
public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$input = $this->input();
if ( is_array( $input['post_ids'] ) ) {
$post_ids = (array) $input['post_ids'];
} elseif ( ! empty( $input['post_ids'] ) ) {
$post_ids = explode( ',', $input['post_ids'] );
} else {
$post_ids = array();
}
if ( count( $post_ids ) < 1 ) {
return new WP_Error( 'empty_post_ids', 'The request must include post_ids' );
}
$result = array(
'results' => array(),
);
foreach ( $post_ids as $post_id ) {
$result['results'][ $post_id ] = $this->delete_post( $path, $blog_id, $post_id );
}
return $result;
}
}
@@ -0,0 +1,82 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoint: /sites/%s/posts/restore
*/
new WPCOM_JSON_API_Bulk_Restore_Post_Endpoint(
array(
'description' => 'Restore multiple posts.',
'group' => 'posts',
'stat' => 'posts:1:bulk-restore',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/posts/restore',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'post_ids' => '(array|string) An array, or comma-separated list, of Post IDs to restore.',
),
'response_format' => array(
'results' => '(object) An object containing results, ',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/restore',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'post_ids' => array( 881, 882 ),
),
),
)
);
/**
* Bulk restore post endpoint class.
*/
class WPCOM_JSON_API_Bulk_Restore_Post_Endpoint extends WPCOM_JSON_API_Update_Post_v1_1_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param object $object - parameter is for making the method signature compatible with its parent class method.
*/
public function callback( $path = '', $blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$input = $this->input();
if ( is_array( $input['post_ids'] ) ) {
$post_ids = (array) $input['post_ids'];
} elseif ( ! empty( $input['post_ids'] ) ) {
$post_ids = explode( ',', $input['post_ids'] );
} else {
$post_ids = array();
}
if ( count( $post_ids ) < 1 ) {
return new WP_Error( 'empty_post_ids', 'The request must include post_ids' );
}
$result = array(
'results' => array(),
);
foreach ( $post_ids as $post_id ) {
$result['results'][ $post_id ] = $this->restore_post( $path, $blog_id, $post_id );
}
return $result;
}
}
@@ -0,0 +1,233 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoints: /sites/%s/comments/status
* /sites/%s/comments/delete
*/
new WPCOM_JSON_API_Bulk_Update_Comments_Endpoint(
array(
'description' => 'Update multiple comment\'s status.',
'group' => 'comments',
'stat' => 'comments:1:bulk-update-status',
'min_version' => '1',
'max_version' => '1',
'method' => 'POST',
'path' => '/sites/%s/comments/status',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'comment_ids' => '(array|string) An array, or comma-separated list, of Comment IDs to update.',
'status' => '(string) The new status value. Allowed values: approved, unapproved, spam, trash',
),
'response_format' => array(
'results' => '(array) An array of updated Comment IDs.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/status',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'comment_ids' => array( 881, 882 ),
'status' => 'approved',
),
),
)
);
new WPCOM_JSON_API_Bulk_Update_Comments_Endpoint(
array(
'description' => 'Permanently delete multiple comments. Note: this request will send non-trashed comments to the trash. Trashed comments will be permanently deleted.',
'group' => 'comments',
'stat' => 'comments:1:bulk-delete',
'min_version' => '1',
'max_version' => '1',
'method' => 'POST',
'path' => '/sites/%s/comments/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'comment_ids' => '(array|string) An array, or comma-separated list, of Comment IDs to delete or trash. (optional)',
'empty_status' => '(string) Force to permanently delete all spam or trash comments. (optional). Allowed values: spam, trash',
),
'response_format' => array(
'results' => '(array) An array of deleted or trashed Comment IDs.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'comment_ids' => array( 881, 882 ),
),
),
)
);
/**
* Bulk update comments endpoint class.
*/
class WPCOM_JSON_API_Bulk_Update_Comments_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$input = $this->input();
if ( isset( $input['comment_ids'] ) && is_array( $input['comment_ids'] ) ) {
$comment_ids = $input['comment_ids'];
} elseif ( isset( $input['comment_ids'] ) && ! empty( $input['comment_ids'] ) ) {
$comment_ids = explode( ',', $input['comment_ids'] );
} else {
$comment_ids = array();
}
$result = array(
'results' => array(),
);
wp_defer_comment_counting( true );
if ( $this->api->ends_with( $path, '/delete' ) ) {
if ( isset( $input['empty_status'] ) && $this->validate_empty_status_param( $input['empty_status'] ) ) {
$result['results'] = $this->delete_all( $input['empty_status'] );
} else {
$result['results'] = $this->bulk_delete_comments( $comment_ids );
}
} else {
$status = isset( $input['status'] ) ? $input['status'] : '';
$result['results'] = $this->bulk_update_comments_status( $comment_ids, $status );
}
wp_defer_comment_counting( false );
return $result;
}
/**
* Determine if the passed comment status is valid or not.
*
* @param string $status - status of passed comment.
*
* @return boolean
*/
public function validate_status_param( $status ) {
return in_array( $status, array( 'approved', 'unapproved', 'pending', 'spam', 'trash' ), true );
}
/**
* Determine if the passed empty status is valid or not.
*
* @param string $empty_status - empty_status of comment.
*
* @return boolean
*/
public function validate_empty_status_param( $empty_status ) {
return in_array( $empty_status, array( 'spam', 'trash' ), true );
}
/**
* Update the status of multiple comments.
*
* @param array $comment_ids Comments to update.
* @param string $status New status value.
*
* @return array Updated comments IDs.
*/
public function bulk_update_comments_status( $comment_ids, $status ) {
if ( count( $comment_ids ) < 1 ) {
return new WP_Error( 'empty_comment_ids', 'The request must include comment_ids', 400 );
}
if ( ! $this->validate_status_param( $status ) ) {
return new WP_Error( 'invalid_status', "Invalid comment status value provided: '$status'.", 400 );
}
$results = array();
foreach ( $comment_ids as $comment_id ) {
if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
continue;
}
$result = false;
switch ( $status ) {
case 'approved':
$result = wp_set_comment_status( $comment_id, 'approve' );
break;
case 'unapproved':
case 'pending':
$result = wp_set_comment_status( $comment_id, 'hold' );
break;
case 'spam':
$result = wp_spam_comment( $comment_id );
break;
case 'trash':
$result = wp_trash_comment( $comment_id );
break;
}
if ( $result ) {
$results[] = $comment_id;
}
}
return $results;
}
/**
* Permanenty delete multiple comments.
*
* Comments are only permanently deleted if trash is disabled or their status is `trash` or `spam`.
* Otherwise they are moved to trash.
*
* @param array $comment_ids Comments to trash or delete.
*
* @return array Deleted comments IDs.
*/
public function bulk_delete_comments( $comment_ids ) {
if ( count( $comment_ids ) < 1 ) {
return new WP_Error( 'empty_comment_ids', 'The request must include comment_ids', 400 );
}
$results = array();
foreach ( $comment_ids as $comment_id ) {
if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
continue;
}
if ( wp_delete_comment( $comment_id ) ) {
$results[] = $comment_id;
}
}
return $results;
}
/**
* Delete all spam or trash comments.
*
* Comments are only permanently deleted if trash is disabled or their status is `trash` or `spam`.
* Otherwise they are moved to trash.
*
* @param string $status Can be `spam` or `trash`.
*
* @return array Deleted comments IDs.
*/
public function delete_all( $status ) {
global $wpdb;
// This could potentially take a long time, so we only want to delete comments created
// before this operation.
// Comments marked `spam` or `trash` after this moment won't be touched.
// Core uses the `pagegen_timestamp` hidden field for this same reason.
$delete_time = gmdate( 'Y-m-d H:i:s' );
$comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_approved = %s AND %s > comment_date_gmt", $status, $delete_time ) );
if ( ! is_countable( $comment_ids ) || array() === $comment_ids ) {
return array();
}
return $this->bulk_delete_comments( $comment_ids );
}
}
@@ -0,0 +1,252 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Comment endpoint.
*
* @todo - can this file be written without overriding global variables?
*
* @phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
*/
/**
* Comment endpoint class.
*/
abstract class WPCOM_JSON_API_Comment_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Comment object array.
*
* @var $comment_object_format
*/
public $comment_object_format = array(
// explicitly document and cast all output.
'ID' => '(int) The comment ID.',
'post' => "(object>post_reference) A reference to the comment's post.",
'author' => '(object>author) The author of the comment.',
'date' => "(ISO 8601 datetime) The comment's creation time.",
'URL' => '(URL) The full permalink URL to the comment.',
'short_URL' => '(URL) The wp.me short URL.',
'content' => '(HTML) <code>context</code> dependent.',
'raw_content' => '(string) Raw comment content.',
'status' => array(
'approved' => 'The comment has been approved.',
'unapproved' => 'The comment has been held for review in the moderation queue.',
'spam' => 'The comment has been marked as spam.',
'trash' => 'The comment is in the trash.',
),
'parent' => "(object>comment_reference|false) A reference to the comment's parent, if it has one.",
'type' => array(
'comment' => 'The comment is a regular comment.',
'trackback' => 'The comment is a trackback.',
'pingback' => 'The comment is a pingback.',
'review' => 'The comment is a product review.',
),
'like_count' => '(int) The number of likes for this comment.',
'i_like' => '(bool) Does the current user like this comment?',
'meta' => '(object) Meta data',
'can_moderate' => '(bool) Whether current user can moderate the comment.',
'i_replied' => '(bool) Has the current user replied to this comment?',
);
/**
* Class constructor.
*
* @param object $args - arguments passed to constructor.
*/
public function __construct( $args ) {
if ( ! $this->response_format ) {
$this->response_format =& $this->comment_object_format;
}
parent::__construct( $args );
}
/**
* Get the comment.
*
* @param int $comment_id - the ID of the comment.
* @param string $context - the context of the comment (displayed or edited).
*/
public function get_comment( $comment_id, $context ) {
global $blog_id;
$comment = get_comment( $comment_id );
if ( ! $comment || is_wp_error( $comment ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
/**
* Filter the comment types that are allowed to be returned.
*
* @since 14.2
*
* @module json-api
*
* @param array $types Array of comment types.
*/
$types = apply_filters( 'jetpack_json_api_comment_types', array( '', 'comment', 'pingback', 'trackback', 'review' ) );
// @todo - can we make this comparison strict without breaking anything?
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( ! in_array( $comment->comment_type, $types ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
$post = get_post( $comment->comment_post_ID );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
$status = wp_get_comment_status( $comment->comment_ID );
// Permissions.
switch ( $context ) {
case 'edit':
if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 );
}
$GLOBALS['post'] = $post;
$comment = get_comment_to_edit( $comment->comment_ID );
foreach ( array( 'comment_author', 'comment_author_email', 'comment_author_url' ) as $field ) {
$comment->$field = htmlspecialchars_decode( $comment->$field, ENT_QUOTES );
}
break;
case 'display':
if ( 'approved' !== $status ) {
$current_user_id = get_current_user_id();
$user_can_read_comment = false;
// @todo - can we make this comparison strict without breaking anything?
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
if ( $current_user_id && $comment->user_id && $current_user_id == $comment->user_id ) {
$user_can_read_comment = true;
} elseif (
$comment->comment_author_email && $comment->comment_author
&&
isset( $this->api->token_details['user'] )
&&
isset( $this->api->token_details['user']['user_email'] )
&&
$this->api->token_details['user']['user_email'] === $comment->comment_author_email
&&
$this->api->token_details['user']['display_name'] === $comment->comment_author
) {
$user_can_read_comment = true;
} else {
$user_can_read_comment = current_user_can( 'edit_posts' );
}
if ( ! $user_can_read_comment ) {
return new WP_Error( 'unauthorized', 'User cannot read unapproved comment', 403 );
}
}
$GLOBALS['post'] = $post;
setup_postdata( $post );
break;
default:
return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
}
$can_view = $this->user_can_view_post( $post->ID );
if ( ! $can_view || is_wp_error( $can_view ) ) {
return $can_view;
}
$GLOBALS['comment'] = $comment;
$response = array();
foreach ( array_keys( $this->comment_object_format ) as $key ) {
switch ( $key ) {
case 'ID':
// explicitly cast all output.
$response[ $key ] = (int) $comment->comment_ID;
break;
case 'post':
$response[ $key ] = (object) array(
'ID' => (int) $post->ID,
'title' => (string) get_the_title( $post->ID ),
'type' => (string) $post->post_type,
'link' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ),
);
break;
case 'author':
$response[ $key ] = (object) $this->get_author( $comment, current_user_can( 'edit_comment', $comment->comment_ID ) );
break;
case 'date':
$response[ $key ] = (string) $this->format_date( $comment->comment_date_gmt, $comment->comment_date );
break;
case 'URL':
$response[ $key ] = (string) esc_url_raw( get_comment_link( $comment->comment_ID ) );
break;
case 'short_URL':
// @todo - pagination
$response[ $key ] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) . "%23comment-{$comment->comment_ID}" );
break;
case 'content':
if ( 'display' === $context ) {
ob_start();
comment_text();
$response[ $key ] = (string) ob_get_clean();
} else {
$response[ $key ] = (string) $comment->comment_content;
}
break;
case 'raw_content':
$response[ $key ] = (string) $comment->comment_content;
break;
case 'status':
$response[ $key ] = (string) $status;
break;
case 'parent': // May be object or false.
$parent = $comment->comment_parent ? get_comment( $comment->comment_parent ) : null;
if ( $parent ) {
$response[ $key ] = (object) array(
'ID' => (int) $parent->comment_ID,
'type' => (string) ( $parent->comment_type ? $parent->comment_type : 'comment' ),
'link' => (string) $this->links->get_comment_link( $blog_id, $parent->comment_ID ),
);
} else {
$response[ $key ] = false;
}
break;
case 'type':
$response[ $key ] = (string) ( $comment->comment_type ? $comment->comment_type : 'comment' );
break;
case 'like_count':
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$response[ $key ] = (int) $this->api->comment_like_count( $blog_id, $post->ID, $comment->comment_ID );
}
break;
case 'i_like':
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$response[ $key ] = (bool) Likes::comment_like_current_user_likes( $blog_id, (int) $comment->comment_ID );
}
break;
case 'meta':
$response[ $key ] = (object) array(
'links' => (object) array(
'self' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID ),
'help' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'help' ),
'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
'post' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $comment->comment_post_ID ),
'replies' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'replies/' ),
'likes' => (string) $this->links->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'likes/' ),
),
);
break;
case 'can_moderate':
$response[ $key ] = (bool) current_user_can( 'edit_comment', $comment_id );
break;
case 'i_replied':
$response[ $key ] = (bool) 0 < get_comments(
array(
'user_id' => get_current_user_id(),
'parent' => $comment->comment_ID,
'count' => true,
)
);
break;
}
}
unset( $GLOBALS['comment'], $GLOBALS['post'] );
return $response;
}
}
@@ -0,0 +1,70 @@
<?php // phpcs:ignore Squiz.Commenting.FileComment.Missing
new WPCOM_JSON_API_Delete_Media_Endpoint(
array(
'description' => 'Delete a piece of media.',
'group' => 'media',
'stat' => 'media:1:delete',
'method' => 'POST',
'path' => '/sites/%s/media/%d/delete',
'deprecated' => true,
'new_version' => '1.1',
'max_version' => '1',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The media ID',
),
'response_format' => array(
'status' => '(string) Returns deleted if the media was successfully deleted',
'id' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'parent' => '(int) ID of the post this media is attached to',
'link' => '(string) URL to the file',
'title' => '(string) File name',
'caption' => '(string) User provided caption of the file',
'description' => '(string) Description of the file',
'metadata' => '(array) Misc array of information about the file, such as exif data or sizes',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/$media_ID/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Delete media endpoint class.
*/
class WPCOM_JSON_API_Delete_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'delete_post', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$item = $this->get_media_item( $media_id );
if ( is_wp_error( $item ) ) {
return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
}
wp_delete_post( $media_id );
$item->status = 'deleted';
return $item;
}
}
@@ -0,0 +1,81 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Delete_Media_v1_1_Endpoint(
array(
'description' => 'Delete a piece of media. Note: Media is deleted and not trashed.',
'group' => 'media',
'stat' => 'media:1:delete',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/media/%d/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The media ID',
),
'response_format' => array(
'status' => '(string) Returns deleted if the media was successfully deleted',
'ID' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'post_ID' => '(int) ID of the post this media is attached to',
'author_ID' => '(int) ID of the user who uploaded the media',
'URL' => '(string) URL to the file',
'guid' => '(string) Unique identifier',
'file' => '(string) File name',
'extension' => '(string) File extension',
'mime_type' => '(string) File mime type',
'title' => '(string) File name',
'caption' => '(string) User-provided caption of the file',
'description' => '(string) Description of the file',
'alt' => '(string) Alternative text for image files.',
'thumbnails' => '(object) Media item thumbnail URL options',
'height' => '(int) (Image & video only) Height of the media item',
'width' => '(int) (Image & video only) Width of the media item',
'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
'videopress_processing_done' => '(bool) (Video only) If the video is Uuploaded on a blog with VideoPress, this will return the status of processing on the Video',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/$media_ID/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Delete media v1_1 endpoint class.
*/
class WPCOM_JSON_API_Delete_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { //phpcs:ignore
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'delete_post', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User is not authorized delete media', 403 );
}
$item = $this->get_media_item_v1_1( $media_id );
if ( is_wp_error( $item ) ) {
return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
}
wp_delete_post( $media_id, true );
$item->status = 'deleted';
return $item;
}
}
@@ -0,0 +1,497 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php';
define( 'REVISION_HISTORY_MAXIMUM_AMOUNT', 5 );
define( 'WP_ATTACHMENT_IMAGE_ALT', '_wp_attachment_image_alt' );
new WPCOM_JSON_API_Edit_Media_v1_2_Endpoint(
array(
'description' => 'Edit a media item.',
'group' => 'media',
'stat' => 'media:1:POST',
'min_version' => '1',
'max_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/media/%d/edit',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'request_format' => array(
'parent_id' => '(int) ID of the post this media is attached to',
'title' => '(string) The file name.',
'caption' => '(string) File caption.',
'description' => '(HTML) Description of the file.',
'alt' => '(string) Alternative text for image files.',
'artist' => '(string) Audio Only. Artist metadata for the audio track.',
'album' => '(string) Audio Only. Album metadata for the audio track.',
'media' => '(media) An object file to attach to the post. To upload media, ' .
'the entire request should be multipart/form-data encoded. ' .
'Multiple media items will be displayed in a gallery. Accepts ' .
'jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. ' .
'Audio and Video may also be available. See <code>allowed_file_types</code> ' .
'in the options response of the site endpoint. ' .
'<br /><br /><strong>Example</strong>:<br />' .
"<code>curl \<br />--form 'title=Image' \<br />--form 'media=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>",
'attrs' => '(object) An Object of attributes (`title`, `description` and `caption`) ' .
'are supported to assign to the media uploaded via the `media` or `media_url`',
'media_url' => '(string) An URL of the image to attach to a post.',
),
'response_format' => array(
'ID' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'post_ID' => '(int) ID of the post this media is attached to',
'author_ID' => '(int) ID of the user who uploaded the media',
'URL' => '(string) URL to the file',
'guid' => '(string) Unique identifier',
'file' => '(string) File name',
'extension' => '(string) File extension',
'mime_type' => '(string) File mime type',
'title' => '(string) File name',
'caption' => '(string) User provided caption of the file',
'description' => '(string) Description of the file',
'alt' => '(string) Alternative text for image files.',
'thumbnails' => '(object) Media item thumbnail URL options',
'height' => '(int) (Image & video only) Height of the media item',
'width' => '(int) (Image & video only) Width of the media item',
'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.',
'revision_history' => '(object) An object with `items` and `original` keys. ' .
'`original` is an object with data about the original image. ' .
'`items` is an array of snapshots of the previous images of this Media. ' .
'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/446',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'title' => 'Updated Title',
),
),
)
);
/**
* Edit media v1_2 endpoint class.
*/
class WPCOM_JSON_API_Edit_Media_v1_2_Endpoint extends WPCOM_JSON_API_Update_Media_v1_1_Endpoint { //phpcs:ignore
/**
* Return an array of mime_type items allowed when the media file is uploaded.
*
* @param array $default_mime_types - array of default mime types.
*
* @return array mime_type array
*/
public static function get_allowed_mime_types( $default_mime_types ) {
return array_unique(
array_merge(
$default_mime_types,
array(
'application/msword', // .doc
'application/vnd.ms-powerpoint', // .ppt, .pps
'application/vnd.ms-excel', // .xls
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.oasis.opendocument.text', // .odt
'application/pdf', // .pdf
)
)
);
}
/**
* Update the media post grabbing the post values from
* the `attrs` parameter
*
* @param int $media_id - post media ID.
* @param array $attrs - `attrs` parameter sent from the client in the request body.
*/
private function update_by_attrs_parameter( $media_id, $attrs ) {
$post_update_action = null;
$insert = array();
// Attributes: Title, Caption, Description.
if ( isset( $attrs['title'] ) ) {
$insert['post_title'] = $attrs['title'];
}
if ( isset( $attrs['caption'] ) ) {
$insert['post_excerpt'] = $attrs['caption'];
}
if ( isset( $attrs['description'] ) ) {
$insert['post_content'] = $attrs['description'];
}
if ( ! empty( $insert ) ) {
$insert['ID'] = $media_id;
$update_action = wp_update_post( (object) $insert );
if ( is_wp_error( $update_action ) ) {
return $update_action;
}
}
// Attributes: Alt.
if ( isset( $attrs['alt'] ) ) {
$alt = wp_strip_all_tags( $attrs['alt'], true );
$post_update_action = update_post_meta( $media_id, WP_ATTACHMENT_IMAGE_ALT, $alt );
if ( is_wp_error( $post_update_action ) ) {
return $post_update_action;
}
}
// Attributes: Artist, Album.
$id3_meta = array();
foreach ( array( 'artist', 'album' ) as $key ) {
if ( isset( $attrs[ $key ] ) ) {
$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
}
}
if ( ! empty( $id3_meta ) ) {
// Before updating metadata, ensure that the item is audio.
$item = $this->get_media_item_v1_1( $media_id );
if ( str_starts_with( $item->mime_type, 'audio/' ) ) {
$update_action = wp_update_attachment_metadata( $media_id, $id3_meta );
if ( is_wp_error( $update_action ) ) {
return $update_action;
}
}
}
return $post_update_action;
}
/**
* Return an object to be used to store into the revision_history
*
* @param object $media_item - media post object.
* @return object the snapshot object
*/
private function get_snapshot( $media_item ) {
$current_file = get_attached_file( $media_item->ID );
$file_paths = pathinfo( $current_file );
$snapshot = array(
'date' => (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
'URL' => (string) wp_get_attachment_url( $media_item->ID ),
'file' => (string) $file_paths['basename'],
'extension' => (string) $file_paths['extension'],
'mime_type' => (string) $media_item->post_mime_type,
'size' => (int) filesize( $current_file ),
);
return (object) $snapshot;
}
/**
* Try to remove the temporal file from the given file array.
*
* @param array $file_array - Array with data about the temporal file.
*/
private function remove_tmp_file( $file_array ) {
if ( file_exists( $file_array['tmp_name'] ) ) {
wp_delete_file( $file_array['tmp_name'] );
}
}
/**
* Save the given temporal file in a local folder.
*
* @param array $file_array - array containing file data.
* @param int $media_id - the media id.
* @param bool $is_upload - True if `$file_array` derives from an upload in `$_FILES`, false if this is a sideload.
* @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong.
*/
private function save_temporary_file( $file_array, $media_id, $is_upload ) {
$tmp_filename = $file_array['tmp_name'];
$is_ok = $is_upload ? is_uploaded_file( $tmp_filename ) : file_exists( $tmp_filename );
if ( ! $is_ok ) {
return new WP_Error( 'invalid_input', 'No media provided in input.' );
}
// add additional mime_types through of the `jetpack_supported_media_sideload_types` filter.
$mime_type_static_filter = array(
'WPCOM_JSON_API_Edit_Media_v1_2_Endpoint',
'get_allowed_mime_types',
);
add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
if (
! $this->is_file_supported_for_sideloading( $tmp_filename ) &&
! file_is_displayable_image( $tmp_filename )
) {
if ( ! $is_upload ) {
wp_delete_file( $tmp_filename );
}
return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
}
remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
// generate a new file name.
$tmp_new_filename = Jetpack_Media::generate_new_filename( $media_id, $file_array['name'] );
// start to create the parameters to move the temporal file.
$overrides = array( 'test_form' => false );
$time = $this->get_time_string_from_guid( $media_id );
$file_array['name'] = $tmp_new_filename;
if ( $is_upload ) {
$file = wp_handle_upload( $file_array, $overrides, $time );
} else {
$file = wp_handle_sideload( $file_array, $overrides, $time );
$this->remove_tmp_file( $file_array );
}
if ( isset( $file['error'] ) ) {
return new WP_Error( 'upload_error', $file['error'] );
}
return $file;
}
/**
* File urls use the post date to generate a folder path.
* Post dates can change, so we use the original date used in the guid
* url so edits can remain in the same folder. In the following function
* we capture a string in the format of `YYYY/MM` from the guid.
*
* For example with a guid of
* "http://test.files.wordpress.com/2016/10/test.png" the resulting string
* would be: "2016/10"
*
* @param int $media_id - the media id.
*
* @return string
*/
private function get_time_string_from_guid( $media_id ) {
// @todo: investigate if we can replace date with gmdate()
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$time = date( 'Y/m', strtotime( current_time( 'mysql' ) ) );
$media = get_post( $media_id );
if ( $media ) {
$pattern = '/\/(\d{4}\/\d{2})\//';
preg_match( $pattern, $media->guid, $matches );
if ( count( $matches ) > 1 ) {
$time = $matches[1];
}
}
return $time;
}
/**
* Get the image from a remote url and then save it locally.
*
* @param int $media_id - media post ID.
* @param string $url - image URL to save locally.
* @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong.
*/
private function build_file_array_from_url( $media_id, $url ) {
if ( ! $url ) {
return null;
}
// if we didn't get a URL, let's bail.
$parsed = wp_parse_url( $url );
if ( empty( $parsed ) ) {
return new WP_Error( 'invalid_url', 'No media provided in url.' );
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$url = wpcom_get_private_file( $url );
}
// save the remote image into a tmp file.
$tmp = download_url( $url );
if ( is_wp_error( $tmp ) ) {
return $tmp;
}
return array(
'name' => basename( $url ),
'tmp_name' => $tmp,
);
}
/**
* Add a new item into revision_history array.
*
* @param object $media_item - media post.
* @param array|WP_Error $file - File data, or WP_Error on error.
* @param bool $has_original_media - condition is the original media has been already added.
* @return bool `true` if the item has been added. Otherwise `false`.
*/
private function register_revision( $media_item, $file, $has_original_media ) {
if (
is_wp_error( $file ) ||
! $has_original_media
) {
return false;
}
add_post_meta( $media_item->ID, Jetpack_Media::WP_REVISION_HISTORY, $this->get_snapshot( $media_item ) );
}
/**
* Restore the original media file.
*
* @param int $media_id - media post ID.
* @param object $original_media - orginal media data.
* @return array - restore media info.
*/
private function restore_original( $media_id, $original_media ) {
$revisions = (array) Jetpack_Media::get_revision_history( $media_id );
$revisions = array_filter(
$revisions,
function ( $revision ) use ( $original_media ) {
return $revision->file !== $original_media->file;
}
);
$criteria = array(
'from' => 0,
'to' => REVISION_HISTORY_MAXIMUM_AMOUNT,
);
Jetpack_Media::remove_items_from_revision_history( $media_id, $criteria, $revisions );
$file = get_attached_file( $media_id );
$file_parts = pathinfo( $file );
$orginal_file = path_join( $file_parts['dirname'], $original_media->file );
$restored_media = array(
'file' => $orginal_file,
'type' => $original_media->mime_type,
);
return $restored_media;
}
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$media_item = get_post( $media_id );
if ( ! $media_item || is_wp_error( $media_item ) ) {
return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
}
if ( is_wp_error( $media_item ) ) {
return $media_item;
}
if ( ! current_user_can( 'upload_files', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$input = $this->input( true );
// Images.
$media_file = isset( $input['media'] ) ? (array) $input['media'] : null;
$media_url = isset( $input['media_url'] ) ? $input['media_url'] : null;
$media_attrs = isset( $input['attrs'] ) ? (array) $input['attrs'] : null;
if ( isset( $media_url ) || $media_file ) {
$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
if ( ! $user_can_upload_files ) {
return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
}
$has_original_media = Jetpack_Media::get_original_media( $media_id );
if ( ! $has_original_media ) {
// The first time that the media is updated
// the original media is stored into the revision_history.
$snapshot = $this->get_snapshot( $media_item );
add_post_meta( $media_id, Jetpack_Media::WP_ORIGINAL_MEDIA, $snapshot, true );
}
// save the temporal file locally.
$is_upload = (bool) $media_file;
$temporal_file = $media_file ? $media_file : $this->build_file_array_from_url( $media_id, $media_url );
if ( is_wp_error( $temporal_file ) ) {
return $temporal_file;
}
// edited media is sent as $media_file and restored media is sent as $media_url
$should_restore = isset( $media_url ) && ! isset( $media_file ) && $has_original_media;
$uploaded_file = $should_restore
? $this->restore_original( $media_id, $has_original_media )
: $this->save_temporary_file( $temporal_file, $media_id, $is_upload );
if ( is_wp_error( $uploaded_file ) ) {
return $uploaded_file;
}
// revision_history control.
$this->register_revision( $media_item, $uploaded_file, $has_original_media );
$uploaded_path = $uploaded_file['file'];
$udpated_mime_type = $uploaded_file['type'];
$was_updated = update_attached_file( $media_id, $uploaded_path );
if ( $was_updated ) {
$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
wp_update_attachment_metadata( $media_id, $new_metadata );
// check maximum amount of revision_history.
Jetpack_Media::limit_revision_history( $media_id, REVISION_HISTORY_MAXIMUM_AMOUNT );
wp_update_post(
(object) array(
'ID' => $media_id,
'post_mime_type' => $udpated_mime_type,
)
);
}
unset( $input['media'] );
unset( $input['media_url'] );
unset( $input['attrs'] );
}
// update media through of `attrs` value it it's defined.
if ( ( $media_file || isset( $media_url ) ) && $media_attrs ) {
$was_updated = $this->update_by_attrs_parameter( $media_id, $media_attrs );
if ( is_wp_error( $was_updated ) ) {
return $was_updated;
}
}
// call parent method.
$response = parent::callback( $path, $blog_id, $media_id );
// expose `revision_history` object.
$response->revision_history = (object) array(
'items' => (array) Jetpack_Media::get_revision_history( $media_id ),
'original' => (object) Jetpack_Media::get_original_media( $media_id ),
);
return $response;
}
}
@@ -0,0 +1,94 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint
*
* @package automattic/jetpack
*/
new WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint(
array(
'description' => 'Get the most recent autosave for a post.',
'group' => '__do_not_document',
'stat' => 'posts:autosave',
'min_version' => '1.1',
'method' => 'GET',
'path' => '/sites/%s/posts/%d/autosave',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'response_format' => array(
'ID' => '(int) autodraft post ID',
'post_ID' => '(int) post ID',
'author_ID' => '(int) author ID',
'title' => '(HTML) The post title.',
'content' => '(HTML) The post content.',
'excerpt' => '(HTML) The post excerpt.',
'preview_URL' => '(string) preview URL for the post',
'modified' => '(ISO 8601 datetime) modified time',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/posts/1/autosave',
)
);
// phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
/**
* Class WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint
*/
class WPCOM_JSON_API_Get_Autosave_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
/**
* Get Autosave callback
* /sites/%s/posts/%d/autosave -> $blog_id, $post_id
*
* @param string $path Path.
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
*
* @return array|int|mixed|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
if ( ! current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
}
$autosave = wp_get_post_autosave( $post->ID );
if ( $autosave ) {
$preview_url = add_query_arg( 'preview', 'true', get_permalink( $post->ID ) );
$nonce = wp_create_nonce( 'post_preview_' . $post->ID );
$preview_url = add_query_arg(
array(
'preview_id' => $post->ID,
'preview_nonce' => $nonce,
),
$preview_url
);
return array(
'ID' => $autosave->ID,
'author_ID' => $autosave->post_author,
'post_ID' => $autosave->post_parent,
'title' => $autosave->post_title,
'content' => $autosave->post_content,
'excerpt' => $autosave->post_excerpt,
'preview_URL' => $preview_url,
'modified' => $this->format_date( $autosave->post_modified_gmt, $autosave->post_modified ),
);
} else {
return new WP_Error( 'not_found', 'No autosaves exist for this post', 404 );
}
}
}
@@ -0,0 +1,86 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoint: /sites/%s/comment-counts
*/
new WPCOM_JSON_API_GET_Comment_Counts_Endpoint(
array(
'description' => 'Get comment counts for each available status',
'group' => 'comments',
'stat' => 'comments:1:comment-counts',
'method' => 'GET',
'path' => '/sites/%s/comment-counts',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'post_id' => '(int) post ID for filtering the comment counts by post',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comment-counts',
'response_format' => array(
'all' => '(int) Combined number of approved and unapproved comments',
'approved' => '(int) Number of approved comments',
'pending' => '(int) Number of unapproved comments',
'trash' => '(int) Number of trash comments',
'spam' => '(int) Number of spam comments',
'post_trashed' => '(int) Number of comments whose parent post has been trashed',
'total_comments' => '(int) Combined number of comments in each category',
),
)
);
/**
* GET Comment Counts endpoint class.
*/
class WPCOM_JSON_API_GET_Comment_Counts_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! get_current_user_id() ) {
return new WP_Error( 'authorization_required', 'An active access token must be used to retrieve comment counts.', 403 );
}
// @phan-suppress-next-line PhanDeprecatedFunction -- @todo Switch to current_user_can_for_site when we drop support for WP 6.6.
if ( ! current_user_can_for_blog( $blog_id, 'edit_posts' ) ) {
return new WP_Error( 'authorization_required', 'You are not authorized to view comment counts for this blog.', 403 );
}
$args = $this->query_args();
// If 0 is passed wp_count_comments will default to fetching counts for the whole site.
$post_id = ! empty( $args['post_id'] ) ? (int) $args['post_id'] : 0;
// Check if post with given id exists.
if ( ! empty( $post_id ) && ! is_object( get_post( $post_id ) ) ) {
return new WP_Error( 'invalid_input', 'Provided post_id does not exist', 400 );
}
$comment_counts = get_object_vars( $this->api->wp_count_comments( $post_id ) );
// Keys coming from wp_count_comments don't match the ones that we use in
// wp-admin and Calypso and are not consistent. Let's normalize the response.
return array(
'all' => (int) $comment_counts['all'],
'approved' => (int) $comment_counts['approved'],
'pending' => (int) $comment_counts['moderated'],
'trash' => (int) $comment_counts['trash'],
'spam' => (int) $comment_counts['spam'],
'post_trashed' => (int) $comment_counts['post-trashed'],
'total_comments' => (int) $comment_counts['total_comments'],
);
}
}
@@ -0,0 +1,54 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoint: /sites/%s/comments/%d -> $blog_id, $comment_id
*/
new WPCOM_JSON_API_Get_Comment_Endpoint(
array(
'description' => 'Get a single comment.',
'group' => 'comments',
'stat' => 'comments:1',
'method' => 'GET',
'path' => '/sites/%s/comments/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$comment_ID' => '(int) The comment ID',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/147564',
)
);
/**
* Get Comment endpoint class.
*/
class WPCOM_JSON_API_Get_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $comment_id - the comment ID.
*/
public function callback( $path = '', $blog_id = 0, $comment_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$return = $this->get_comment( $comment_id, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'comments' );
return $return;
}
}
@@ -0,0 +1,66 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoint: /sites/%s/comment-history/%d
*/
new WPCOM_JSON_API_GET_Comment_History_Endpoint(
array(
'description' => 'Get the audit history for given comment',
'group' => 'comments',
'stat' => 'comments:1:comment-history',
'method' => 'GET',
'path' => '/sites/%s/comment-history/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$comment_ID' => '(int) The comment ID',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comment-history/11',
'response_format' => array(
'comment_history' => '(array) Array of arrays representing the comment history objects.',
),
)
);
/**
* GET Comment History endpoint.
*/
class WPCOM_JSON_API_GET_Comment_History_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $comment_id - the comment ID.
*/
public function callback( $path = '', $blog_id = 0, $comment_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! get_current_user_id() ) {
return new WP_Error( 'authorization_required', 'An active access token must be used to retrieve comment history.', 403 );
}
// @phan-suppress-next-line PhanDeprecatedFunction -- @todo Switch to current_user_can_for_site when we drop support for WP 6.6.
if ( ! current_user_can_for_blog( $blog_id, 'edit_posts' ) ) {
return new WP_Error( 'authorization_required', 'You are not authorized to view comment history on this blog.', 403 );
}
if ( ! method_exists( 'Akismet', 'get_comment_history' ) ) {
return new WP_Error( 'akismet_required', 'Akismet plugin must be active for this feature to work', 503 );
}
$comment_history = Akismet::get_comment_history( $comment_id );
foreach ( $comment_history as &$item ) {
// Times are stored as floating point values in microseconds.
// We don't need that precision on the client so let's get rid of the decimal part.
$item['time'] = (int) $item['time'];
}
return array( 'comment_history' => $comment_history );
}
}
@@ -0,0 +1,195 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Comments_Tree_Endpoint(
array(
'description' => 'Get a comments tree for site.',
'max_version' => '1',
'new_version' => '1.1',
'group' => 'comments-tree',
'stat' => 'comments-tree:1',
'method' => 'GET',
'path' => '/sites/%s/comments-tree',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, unapproved, pending, trash, spam).',
),
'response_format' => array(
'comments_count' => '(int) Total number of comments on the site',
'comments_tree' => '(array) Array of arrays representing the comments tree for given site (max 50000)',
'trackbacks_count' => '(int) Total number of trackbacks on the site',
'trackbacks_tree' => '(array) Array of arrays representing the trackbacks tree for given site (max 50000)',
'pingbacks_count' => '(int) Total number of pingbacks on the site',
'pingbacks_tree' => '(array) Array of arrays representing the pingbacks tree for given site (max 50000)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments-tree?status=approved',
)
);
/**
* GET comments tree endpoint class.
*/
class WPCOM_JSON_API_Get_Comments_Tree_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Retrieves a list of comment data for a given site.
*
* @param string $status Filter by status: all, approved, pending, spam or trash.
* @param int $start_at first comment to search from going back in time.
*
* @return array
*/
public function get_site_tree( $status, $start_at = PHP_INT_MAX ) {
global $wpdb;
$max_comment_count = 50000;
$db_status = $this->get_comment_db_status( $status );
$db_comment_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT comment_ID, comment_post_ID, comment_parent, comment_type ' .
"FROM $wpdb->comments AS comments " .
"INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " .
"WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " .
'ORDER BY comment_ID DESC ' .
'LIMIT %d',
(int) $start_at,
$db_status,
$db_status,
$max_comment_count
),
ARRAY_N
);
$comments = array();
$trackbacks = array();
$pingbacks = array();
foreach ( $db_comment_rows as $row ) {
list( $comment_id, $comment_post_id, $comment_parent, $comment_type ) = $row;
switch ( $comment_type ) {
case 'trackback':
$trackbacks[] = array( $comment_id, $comment_post_id, $comment_parent );
break;
case 'pingback':
$pingbacks[] = array( $comment_id, $comment_post_id, $comment_parent );
break;
default:
$comments[] = array( $comment_id, $comment_post_id, $comment_parent );
}
}
return array(
'comments_count' => $this->get_site_tree_total_count( $status, 'comment' ),
'comments_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $comments ),
'trackbacks_count' => $this->get_site_tree_total_count( $status, 'trackback' ),
'trackbacks_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $trackbacks ),
'pingbacks_count' => $this->get_site_tree_total_count( $status, 'pingback' ),
'pingbacks_tree' => array_map( array( $this, 'array_map_all_as_ints' ), $pingbacks ),
);
}
/**
* Ensure all values are integers.
*
* @param array $comments Collection of comments.
*
* @return array Comments with values as integers.
*/
public function array_map_all_as_ints( $comments ) {
return array_map( 'intval', $comments );
}
/**
* Retrieves a total count of comments by type for the given site.
*
* @param string $status Filter by status: all, approved, pending, spam or trash.
* @param string $type Comment type: 'trackback', 'pingback', or 'comment'.
*
* @return int Total count of comments for a site.
*/
public function get_site_tree_total_count( $status, $type ) {
global $wpdb;
$db_status = $this->get_comment_db_status( $status );
$type = $this->get_sanitized_comment_type( $type );
$result = $wpdb->get_var(
$wpdb->prepare(
'SELECT COUNT(1) ' .
"FROM $wpdb->comments AS comments " .
"INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " .
"WHERE comment_type = %s AND ( %s = 'all' OR comment_approved = %s )",
$type,
$db_status,
$db_status
)
);
return (int) $result;
}
/**
* Ensure a valid status is converted to a database-supported value if necessary.
*
* @param string $status Should be one of: all, approved, pending, spam or trash.
*
* @return string Corresponding value that exists in database.
*/
public function get_comment_db_status( $status ) {
if ( 'approved' === $status ) {
return '1';
}
if ( 'pending' === $status || 'unapproved' === $status ) {
return '0';
}
return $status;
}
/**
* Determine if the passed comment status is valid or not.
*
* @param string $status - comment status.
*
* @return boolean
*/
public function validate_status_param( $status ) {
return in_array( $status, array( 'all', 'approved', 'unapproved', 'pending', 'spam', 'trash' ), true );
}
/**
* Sanitize a given comment type.
*
* @param string $type Comment type: can be 'trackback', 'pingback', or 'comment'.
*
* @return string Sanitized comment type.
*/
public function get_sanitized_comment_type( $type = 'comment' ) {
if ( in_array( $type, array( 'trackback', 'pingback', 'comment' ), true ) ) {
return $type;
}
return 'comment';
}
/**
* Endpoint callback for /sites/%s/comments-tree
*
* @param string $path - the api path.
* @param int $blog_id - the blog id.
*
* @return array Site tree results by status.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$comment_status = empty( $args['status'] ) ? 'all' : $args['status'];
if ( ! $this->validate_status_param( $comment_status ) ) {
return new WP_Error( 'invalid_status', "Invalid comment status value provided: '$comment_status'.", 400 );
}
return $this->get_site_tree( $comment_status );
}
}
@@ -0,0 +1,100 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint(
array(
'description' => 'Get a comments tree for site.',
'min_version' => '1.1',
'max_version' => '1.1',
'group' => 'comments-tree',
'stat' => 'comments-tree:1',
'method' => 'GET',
'path' => '/sites/%s/comments-tree',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, pending, trash, spam).',
),
'response_format' => array(
'comments_count' => '(int) Total number of comments on the site',
'comments_tree' => '(array) Array of post IDs representing the comments tree for given site (max 50000)',
'trackbacks_count' => '(int) Total number of trackbacks on the site',
'trackbacks_tree' => '(array) Array of post IDs representing the trackbacks tree for given site (max 50000)',
'pingbacks_count' => '(int) Total number of pingbacks on the site',
'pingbacks_tree' => '(array) Array of post IDs representing the pingbacks tree for given site (max 50000)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/comments-tree?status=approved',
)
);
/**
* GET comments tree v1_1 endpoint.
*/
class WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_Endpoint { // phpcs:ignore
/**
* Retrieves a list of comment data for a given site.
*
* @param string $status Filter by status: all, approved, pending, spam or trash.
* @param int $start_at first comment to search from going back in time.
*
* @return array
*/
public function get_site_tree( $status, $start_at = PHP_INT_MAX ) {
global $wpdb;
$max_comment_count = 50000;
$db_status = $this->get_comment_db_status( $status );
$db_comment_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT comment_ID, comment_post_ID, comment_parent, comment_type ' .
"FROM $wpdb->comments AS comments " .
"INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " .
"WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " .
'ORDER BY comment_ID DESC ' .
'LIMIT %d',
(int) $start_at,
$db_status,
$db_status,
$max_comment_count
),
ARRAY_N
);
$comments = array();
$trackbacks = array();
$pingbacks = array();
foreach ( $db_comment_rows as $row ) {
$comment_id = (int) $row[0];
$comment_post_id = (int) $row[1];
$comment_parent_id = (int) $row[2];
if ( ! isset( $comments[ $comment_post_id ] ) ) {
$comments[ $comment_post_id ] = array( array(), array() );
}
switch ( $row[3] ) {
case 'trackback':
$trackbacks[ $comment_post_id ][] = $comment_id;
break;
case 'pingback':
$pingbacks[ $comment_post_id ][] = $comment_id;
break;
default:
if ( 0 === $comment_parent_id ) {
$comments[ $comment_post_id ][0][] = $comment_id;
} else {
$comments[ $comment_post_id ][1][] = array( $comment_id, $comment_parent_id );
}
}
}
return array(
'comments_count' => $this->get_site_tree_total_count( $status, 'comment' ),
'comments_tree' => $comments,
'trackbacks_count' => $this->get_site_tree_total_count( $status, 'trackback' ),
'trackbacks_tree' => $trackbacks,
'pingbacks_count' => $this->get_site_tree_total_count( $status, 'pingback' ),
'pingbacks_tree' => $pingbacks,
);
}
}
@@ -0,0 +1,160 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Comments_Tree_v1_2_Endpoint(
array(
'description' => 'Get a comments tree for site.',
'min_version' => '1.2',
'max_version' => '1.2',
'group' => 'comments-tree',
'stat' => 'comments-tree:1',
'method' => 'GET',
'path' => '/sites/%s/comments-tree',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'post_id' => '(int) Filter returned comments by a post.',
'status' => '(string) Filter returned comments based on this value (allowed values: all, approved, pending, trash, spam).',
),
'response_format' => array(
'comments_tree' => '(array) Array of post IDs representing the comments tree for given site or post (max 50000)',
'trackbacks_tree' => '(array) Array of post IDs representing the trackbacks tree for given site or post (max 50000)',
'pingbacks_tree' => '(array) Array of post IDs representing the pingbacks tree for given site or post (max 50000)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/comments-tree?&status=approved&post_id=123',
)
);
/**
* Get comments tree v1_2 endpoint class.
*/
class WPCOM_JSON_API_Get_Comments_Tree_v1_2_Endpoint extends WPCOM_JSON_API_Get_Comments_Tree_v1_1_Endpoint { // phpcs:ignore
/**
* Retrieves a list of comment data.
*
* @param array $args {
* Optional. Arguments to control behavior. Default empty array.
*
* @type int $max_comment_count Maximum number of comments returned.
* @type int $post_id Filter by post.
* @type int $start_at First comment to search from going back in time.
* @type string $status Filter by status: all, approved, pending, spam or trash.
* }
*
* @return array
*/
public function get_site_tree_v1_2( $args = array() ) {
global $wpdb;
$defaults = array(
'max_comment_count' => 50000,
'post_id' => null,
'start_at' => PHP_INT_MAX,
'status' => 'all',
);
$args = wp_parse_args( $args, $defaults );
$db_status = $this->get_comment_db_status( $args['status'] );
if ( ! empty( $args['post_id'] ) ) {
$db_comment_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT comment_ID, comment_parent, comment_type ' .
"FROM $wpdb->comments AS comments " .
"WHERE comment_ID <= %d AND comment_post_ID = %d AND ( %s = 'all' OR comment_approved = %s ) " .
'ORDER BY comment_ID DESC ' .
'LIMIT %d',
(int) $args['start_at'],
(int) $args['post_id'],
$db_status,
$db_status,
$args['max_comment_count']
),
ARRAY_N
);
} else {
$db_comment_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT comment_ID, comment_parent, comment_type, comment_post_ID ' .
"FROM $wpdb->comments AS comments " .
"INNER JOIN $wpdb->posts AS posts ON comments.comment_post_ID = posts.ID " .
"WHERE comment_ID <= %d AND ( %s = 'all' OR comment_approved = %s ) " .
'ORDER BY comment_ID DESC ' .
'LIMIT %d',
(int) $args['start_at'],
$db_status,
$db_status,
$args['max_comment_count']
),
ARRAY_N
);
}
$comments = array();
$trackbacks = array();
$pingbacks = array();
foreach ( $db_comment_rows as $row ) {
$comment_id = (int) $row[0];
$comment_parent_id = (int) $row[1];
$comment_post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : (int) $row[3];
if ( ! isset( $comments[ $comment_post_id ] ) ) {
$comments[ $comment_post_id ] = array( array(), array() );
}
switch ( $row[2] ) {
case 'trackback':
$trackbacks[ $comment_post_id ][] = $comment_id;
break;
case 'pingback':
$pingbacks[ $comment_post_id ][] = $comment_id;
break;
default:
if ( 0 === $comment_parent_id ) {
$comments[ $comment_post_id ][0][] = $comment_id;
} else {
$comments[ $comment_post_id ][1][] = array( $comment_id, $comment_parent_id );
}
}
}
return array(
'comments_tree' => $comments,
'trackbacks_tree' => $trackbacks,
'pingbacks_tree' => $pingbacks,
);
}
/**
* Endpoint callback for /sites/%s/comments-tree
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*
* @return array Site or post tree results by status.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$filters = array();
if ( ! empty( $args['status'] ) ) {
if ( ! $this->validate_status_param( $args['status'] ) ) {
return new WP_Error( 'invalid_status', 'Invalid comment status value provided: ' . $args['status'] . '.', 400 );
}
$filters['status'] = $args['status'];
}
if ( ! empty( $args['post_id'] ) ) {
if ( get_post( absint( $args['post_id'] ) ) === null ) {
return new WP_Error( 'invalid_post', 'Invalid post', 400 );
}
$filters['post_id'] = absint( $args['post_id'] );
}
return $this->get_site_tree_v1_2( $filters );
}
}
@@ -0,0 +1,64 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Custom Css endpoint
*
* Endpoint: https://public-api.wordpress.com/rest/v1.1/sites/$site/customcss/
*/
new WPCOM_JSON_API_Get_CustomCss_Endpoint(
array(
'description' => 'Retrieve custom-css data for a site.',
'group' => '__do_not_document',
'stat' => 'customcss:1:get',
'method' => 'GET',
'min_version' => '1.1',
'path' => '/sites/%s/customcss',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'response_format' => array(
'css' => '(string) The raw CSS.',
'preprocessor' => '(string) The name of the preprocessor if any.',
'add_to_existing' => '(bool) False to skip the existing styles.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/customcss',
'example_response' => '
{
"css": ".site-title { color: #fff; }",
"preprocessor": "sass",
"add_to_existing": "true"
}',
)
);
/**
* GET Custom CSS Endpoint
*/
class WPCOM_JSON_API_Get_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
// Switch to the given blog.
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = array(
'css' => Jetpack_Custom_CSS::get_css(),
'preprocessor' => Jetpack_Custom_CSS::get_preprocessor_key(),
'add_to_existing' => ! Jetpack_Custom_CSS::skip_stylesheet(),
);
$defaults = array(
'css' => '',
'preprocessor' => '',
'add_to_existing' => true,
);
return wp_parse_args( $args, $defaults );
}
}
@@ -0,0 +1,62 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Media_Endpoint(
array(
'description' => 'Get a single media item (by ID).',
'group' => 'media',
'stat' => 'media:1',
'method' => 'GET',
'path' => '/sites/%s/media/%d',
'deprecated' => true,
'new_version' => '1.1',
'max_version' => '1',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'response_format' => array(
'id' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'parent' => '(int) ID of the post this media is attached to',
'link' => '(string) URL to the file',
'title' => '(string) Filename',
'caption' => '(string) User-provided caption of the file',
'description' => '(string) Description of the file',
'metadata' => '(array) Array of metadata about the file, such as Exif data or sizes',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/934',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* GET Media endpoint class.
*/
class WPCOM_JSON_API_Get_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// upload_files can probably be used for other endpoints but we want contributors to be able to use media too.
if ( ! current_user_can( 'edit_posts', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
return $this->get_media_item( $media_id );
}
}
@@ -0,0 +1,80 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Media_v1_1_Endpoint(
array(
'description' => 'Get a single media item (by ID).',
'group' => 'media',
'stat' => 'media:1',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'GET',
'path' => '/sites/%s/media/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'response_format' => array(
'ID' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'post_ID' => '(int) ID of the post this media is attached to',
'author_ID' => '(int) ID of the user who uploaded the media',
'URL' => '(string) URL to the file',
'guid' => '(string) Unique identifier',
'file' => '(string) Filename',
'extension' => '(string) File extension',
'mime_type' => '(string) File MIME type',
'title' => '(string) Filename',
'caption' => '(string) User-provided caption of the file',
'description' => '(string) Description of the file',
'alt' => '(string) Alternative text for image files.',
'thumbnails' => '(object) Media item thumbnail URL options',
'height' => '(int) (Image & video only) Height of the media item',
'width' => '(int) (Image & video only) Width of the media item',
'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
'rating' => '(string) (Video only) VideoPress rating of the video',
'display_embed' => '(string) Video only. Whether to share or not the video.',
'allow_download' => '(string) Video only. Whether the video can be downloaded or not.',
'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/934',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* GET Media v1_1 endpoint.
*/
class WPCOM_JSON_API_Get_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { //phpcs:ignore
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
// upload_files can probably be used for other endpoints but we want contributors to be able to use media too.
if ( ! current_user_can( 'edit_posts', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
return $this->get_media_item_v1_1( $media_id );
}
}
@@ -0,0 +1,87 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php';
new WPCOM_JSON_API_Get_Media_v1_2_Endpoint(
array(
'description' => 'Get a single media item (by ID).',
'group' => 'media',
'stat' => 'media:1',
'min_version' => '1.2',
'max_version' => '1.2',
'method' => 'GET',
'path' => '/sites/%s/media/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'response_format' => array(
'ID' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'post_ID' => '(int) ID of the post this media is attached to',
'author_ID' => '(int) ID of the user who uploaded the media',
'URL' => '(string) URL to the file',
'guid' => '(string) Unique identifier',
'file' => '(string) Filename',
'extension' => '(string) File extension',
'mime_type' => '(string) File MIME type',
'title' => '(string) Filename',
'caption' => '(string) User-provided caption of the file',
'description' => '(string) Description of the file',
'alt' => '(string) Alternative text for image files.',
'thumbnails' => '(object) Media item thumbnail URL options',
'height' => '(int) (Image & video only) Height of the media item',
'width' => '(int) (Image & video only) Width of the media item',
'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
'rating' => '(string) (Video only) VideoPress rating of the video',
'display_embed' => '(string) Video only. Whether to share or not the video.',
'allow_download' => '(string) Video only. Whether the video can be downloaded or not.',
'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.',
'revision_history' => '(object) An object with `items` and `original` keys. ' .
'`original` is an object with data about the original image. ' .
'`items` is an array of snapshots of the previous images of this Media. ' .
'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/934',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* GET Media v1_2 endpoint class.
*/
class WPCOM_JSON_API_Get_Media_v1_2_Endpoint extends WPCOM_JSON_API_Get_Media_v1_1_Endpoint { //phpcs:ignore
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $media_id - the media ID.
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$response = parent::callback( $path, $blog_id, $media_id );
if ( is_wp_error( $response ) ) {
return $response;
}
$media_item = get_post( $media_id );
$response->modified = (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified );
// expose `revision_history` object.
$response->revision_history = (object) array(
'items' => (array) Jetpack_Media::get_revision_history( $media_id ),
'original' => (object) Jetpack_Media::get_original_media( $media_id ),
);
return $response;
}
}
@@ -0,0 +1,171 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_GET_Post_Counts_V1_1_Endpoint(
array(
'description' => 'Get number of posts in the post type groups by post status',
'group' => 'sites',
'stat' => 'sites:X:post-counts:X',
'force' => 'wpcom',
'method' => 'GET',
'min_version' => '1.1',
'max_version' => '1.2',
'path' => '/sites/%s/post-counts/%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_type' => '(string) Post Type',
),
'query_parameters' => array(
'context' => false,
'author' => '(int) author ID',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/post-counts/page',
'response_format' => array(
'counts' => array(
'all' => '(array) Number of posts by any author in the post type grouped by post status',
'mine' => '(array) Number of posts by the current user in the post type grouped by post status',
),
),
)
);
/**
* GET Post Counts v1_1 endpoint class.
*/
class WPCOM_JSON_API_GET_Post_Counts_V1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Whitelist array.
*
* @var string[]
*/
private $allowlist = array( 'publish' );
/**
* Build SQL query
*
* This function must `$wpdb->prepare` the query. The return is expected to be prepared by consuming functions.
*
* @param string $post_type - post type.
* @param int $user_id - the user ID.
* @return string SQL query
*/
private function buildCountsQuery( $post_type = 'post', $user_id = null ) {
global $wpdb;
$query = 'SELECT post_status as status, count(*) as count ';
$query .= "FROM {$wpdb->posts} ";
$query .= 'WHERE post_type = %s ';
if ( isset( $user_id ) ) {
$query .= 'AND post_author = %d ';
}
$query .= 'GROUP BY status';
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This is properly prepared, except the query is constructed in the variable, throwing the PHPCS error.
return $wpdb->prepare( $query, $post_type, $user_id );
}
/**
* Retrive counts using wp_cache
*
* @param string $post_type - thge post type.
* @param int $id - the ID.
*/
private function retrieveCounts( $post_type, $id = null ) {
if ( ! isset( $id ) ) {
$counts = array();
foreach ( (array) wp_count_posts( $post_type ) as $status => $count ) {
if ( in_array( $status, $this->allowlist, true ) && $count > 0 ) {
$counts[ $status ] = (int) $count;
}
}
return $counts;
}
global $wpdb;
$key = 'rest-api-' . $id . '-' . _count_posts_cache_key( $post_type );
$counts = wp_cache_get( $key, 'counts' );
if ( false === $counts ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- buildCountsQuery prepares the query.
$results = $wpdb->get_results( $this->buildCountsQuery( $post_type, $id ) );
$counts = $this->filterStatusesByWhiteslist( $results );
wp_cache_set( $key, $counts, 'counts' );
}
return $counts;
}
/**
* Filter statuses by whiteslist.
*
* @param array $in - the post we're checking.
*/
private function filterStatusesByWhiteslist( $in ) {
$return = array();
foreach ( $in as $result ) {
if ( in_array( $result->status, $this->allowlist, true ) ) {
$return[ $result->status ] = (int) $result->count;
}
}
return $return;
}
/**
*
* API callback.
*
* /sites/%s/post-counts/%s
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param string $post_type - the post type.
*/
public function callback( $path = '', $blog_id = 0, $post_type = 'post' ) {
if ( ! get_current_user_id() ) {
return new WP_Error( 'authorization_required', __( 'An active access token must be used to retrieve post counts.', 'jetpack' ), 403 );
}
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// @todo see if we can use a strict comparison here.
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( ! in_array( $post_type, array( 'post', 'revision', 'page', 'any' ), true ) && defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
if ( ! post_type_exists( $post_type ) ) {
return new WP_Error( 'unknown_post_type', __( 'Unknown post type requested.', 'jetpack' ), 404 );
}
$args = $this->query_args();
$mine_ID = get_current_user_id(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
if ( current_user_can( 'edit_posts' ) ) {
array_push( $this->allowlist, 'draft', 'future', 'pending', 'private', 'trash' );
}
$return = array(
'counts' => (array) array(
'all' => (object) $this->retrieveCounts( $post_type ),
'mine' => (object) $this->retrieveCounts( $post_type, $mine_ID ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
),
);
// Author.
if ( isset( $args['author'] ) ) {
$author_ID = $args['author']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$return['counts']['author'] = (object) $this->retrieveCounts( $post_type, $author_ID ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
}
return (object) $return;
}
}
@@ -0,0 +1,104 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Endpoints: /sites/%s/posts/%d -> $blog_id, $post_id
* /sites/%s/posts/name:%s -> $blog_id, $post_id // not documented
* /sites/%s/posts/slug:%s -> $blog_id, $post_id
*/
new WPCOM_JSON_API_Get_Post_Endpoint(
array(
'description' => 'Get a single post (by ID).',
'group' => 'posts',
'stat' => 'posts:1',
'new_version' => '1.1',
'max_version' => '1',
'method' => 'GET',
'path' => '/sites/%s/posts/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7',
)
);
new WPCOM_JSON_API_Get_Post_Endpoint(
array(
'description' => 'Get a single post (by name)',
'group' => '__do_not_document',
'stat' => 'posts:name',
'method' => 'GET',
'path' => '/sites/%s/posts/name:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_name' => '(string) The post name (a.k.a. slug)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/name:blogging-and-stuff',
)
);
new WPCOM_JSON_API_Get_Post_Endpoint(
array(
'description' => 'Get a single post (by slug).',
'group' => 'posts',
'stat' => 'posts:slug',
'new_version' => '1.1',
'max_version' => '1',
'method' => 'GET',
'path' => '/sites/%s/posts/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_slug' => '(string) The post slug (a.k.a. sanitized name)',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff',
)
);
/**
* Get post endpoint class.
*/
class WPCOM_JSON_API_Get_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $post_id - the post ID.
*/
public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
if ( ! str_contains( $path, '/posts/slug:' ) && ! str_contains( $path, '/posts/name:' ) ) {
$get_by = 'ID';
} else {
$get_by = 'name';
}
$return = $this->get_post_by( $get_by, $post_id, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
if ( ! $this->current_user_can_access_post_type( $return['type'], $args['context'] ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'posts' );
return $return;
}
}
@@ -0,0 +1,113 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Post_v1_1_Endpoint(
array(
'description' => 'Get a single post (by ID).',
'min_version' => '1.1',
'max_version' => '1.1',
'group' => 'posts',
'stat' => 'posts:1',
'method' => 'GET',
'path' => '/sites/%s/posts/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/7',
)
);
new WPCOM_JSON_API_Get_Post_v1_1_Endpoint(
array(
'description' => 'Get a single post (by slug).',
'min_version' => '1.1',
'max_version' => '1.1',
'group' => 'posts',
'stat' => 'posts:slug',
'method' => 'GET',
'path' => '/sites/%s/posts/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_slug' => '(string) The post slug (a.k.a. sanitized name)',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff',
)
);
/**
* Get Post v1_1 endpoint.
*/
class WPCOM_JSON_API_Get_Post_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { // phpcs:ignore
/**
*
* API callback.
*
* /sites/%s/posts/%d -> $blog_id, $post_id
* /sites/%s/posts/slug:%s -> $blog_id, $post_id
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $post_id - the post ID.
*/
public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
if ( str_contains( $path, '/posts/slug:' ) ) {
$site = $this->get_platform()->get_site( $blog_id );
$post_id = $site->get_post_id_by_name( $post_id );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
}
return $this->fetch_post( $blog_id, $post_id, $args['context'] );
}
/**
* Helper function to fetch the content of a post. User validation
* should be handled by the caller.
*
* @param int $blog_id The blog ID for the post.
* @param int $post_id The post ID.
* @param string $context The context we're fetching for.
* @return array|SAL_Post|WP_Error
*/
public function fetch_post( $blog_id, $post_id, $context ) {
$site = $this->get_platform()->get_site( $blog_id );
if (
defined( 'IS_WPCOM' )
&& IS_WPCOM
&& ! in_array( get_post_type( $post_id ), array( false, 'post', 'revision' ), true )
) {
$this->load_theme_functions();
}
$post = $this->get_post_by( 'ID', $post_id, $context );
if ( ! $post || is_wp_error( $post ) ) {
return $post;
}
if ( ! $site->current_user_can_access_post_type( $post['type'], $context ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'posts' );
return $post;
}
}
@@ -0,0 +1,119 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_GET_Site_V1_2_Endpoint(
array(
'description' => 'Get information about a site.',
'group' => 'sites',
'stat' => 'sites:X',
'allowed_if_flagged' => true,
'method' => 'GET',
'min_version' => '1.2',
'path' => '/sites/%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'query_parameters' => array(
'context' => false,
'filters' => '(string) Optional. Returns sites that satisfy the given filters only. Example: filters=jetpack,atomic,wpcom',
),
'response_format' => WPCOM_JSON_API_GET_Site_V1_2_Endpoint::$site_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/',
)
);
/**
* GET Site v1_2 endpoint.
*/
class WPCOM_JSON_API_GET_Site_V1_2_Endpoint extends WPCOM_JSON_API_GET_Site_Endpoint {
/**
* Site format array.
*
* @var array $site_format
*/
public static $site_format = array(
'ID' => '(int) Site ID',
'name' => '(string) Title of site',
'description' => '(string) Tagline or description of site',
'URL' => '(string) Full URL to the site',
'capabilities' => '(array) Array of capabilities for the current user on this site.',
'jetpack' => '(bool) Whether the site is a Jetpack site or not',
'jetpack_connection' => '(bool) Whether the site is connected to WP.com via `jetpack-connection`',
'is_multisite' => '(bool) Whether the site is a Multisite site or not. Always true for WP.com sites.',
'site_owner' => '(int) User ID of the site owner',
'post_count' => '(int) The number of posts the site has',
'subscribers_count' => '(int) The number of subscribers the site has',
'locale' => '(string) Primary locale code of the site',
'icon' => '(array) An array of icon formats for the site',
'logo' => '(array) The site logo, set in the Customizer',
'visible' => '(bool) If this site is visible in the user\'s site list',
'is_private' => '(bool) If the site is a private site or not',
'is_coming_soon' => '(bool) If the site is a "coming soon" site or not',
'single_user_site' => '(bool) Whether the site is single user. Only returned for WP.com sites and for Jetpack sites with version 3.4 or higher.',
'is_vip' => '(bool) If the site is a VIP site or not.',
'is_following' => '(bool) If the current user is subscribed to this site in the reader',
'organization_id' => '(int) P2 Organization identifier.',
'options' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site. Note: Post formats is deprecated, please see /sites/$id/post-formats/',
'plan' => '(array) Details of the current plan for this site.',
'products' => '(array) Details of the current products for this site.',
'zendesk_site_meta' => '(array) Site meta data for Zendesk.',
'updates' => '(array) An array of available updates for plugins, themes, wordpress, and languages.',
'jetpack_modules' => '(array) A list of active Jetpack modules.',
'meta' => '(object) Meta data',
'quota' => '(array) An array describing how much space a user has left for uploads',
'launch_status' => '(string) A string describing the launch status of a site',
'site_migration' => '(array) Data about any migration into the site.',
'is_fse_active' => '(bool) If the site has Full Site Editing active or not.',
'is_fse_eligible' => '(bool) If the site is capable of Full Site Editing or not',
'is_core_site_editor_enabled' => '(bool) If the site has the core site editor enabled.',
'is_wpcom_atomic' => '(bool) If the site is a WP.com Atomic one.',
'is_wpcom_staging_site' => '(bool) If the site is a WP.com staging site.',
'was_ecommerce_trial' => '(bool) If the site ever used an eCommerce trial.',
'was_upgraded_from_trial' => '(bool) If the site ever upgraded to a paid plan from a trial.',
'was_migration_trial' => '(bool) If the site ever used a migration trial.',
'was_hosting_trial' => '(bool) If the site ever used a hosting trial.',
'is_deleted' => '(bool) If the site flagged as deleted.',
'is_a4a_client' => '(bool) If the site is an A4A client site.',
'is_a4a_dev_site' => '(bool) If the site is an A4A dev site.',
);
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
add_filter( 'sites_site_format', array( $this, 'site_format' ) );
// Site filtering is a WPCOM concept, once a request gets anywhere else it should just be returned
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// Apply filter here, return same error as switch_to_blog_and_validate_user if blog is not found.
require_lib( 'site-filter' );
$filters = Site_Filter::process_query_arg( $this->query_args() );
if ( is_wp_error( $filters ) ) {
return $filters;
}
if ( ! empty( $filters ) && ! Site_Filter::filter_blog( $this->api->get_blog_id( $blog_id ), $filters ) ) {
return new WP_Error( 'unknown_blog', 'Unknown blog', 404 );
}
}
return parent::callback( $path, $blog_id );
}
/**
* Site format.
*
* @param string $format - the format.
*/
public function site_format( $format ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return self::$site_format;
}
}
@@ -0,0 +1,181 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Taxonomies_Endpoint(
array(
'description' => "Get a list of a site's categories.",
'group' => 'taxonomy',
'stat' => 'categories',
'method' => 'GET',
'path' => '/sites/%s/categories',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=100) The number of categories to return. Limit: 1000.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of categories. Takes precedence over the <code>offset</code> parameter.',
'search' => '(string) Limit response to include only categories whose names or slugs match the provided search query.',
'order' => array(
'ASC' => 'Return categories in ascending order.',
'DESC' => 'Return categories in descending order.',
),
'order_by' => array(
'name' => 'Order by the name of each category.',
'count' => 'Order by the number of posts in each category.',
),
),
'response_format' => array(
'found' => '(int) The number of categories returned.',
'categories' => '(array) Array of category objects.',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/categories/?number=5',
)
);
new WPCOM_JSON_API_Get_Taxonomies_Endpoint(
array(
'description' => "Get a list of a site's tags.",
'group' => 'taxonomy',
'stat' => 'tags',
'method' => 'GET',
'path' => '/sites/%s/tags',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=100) The number of tags to return. Limit: 1000.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of tags. Takes precedence over the <code>offset</code> parameter.',
'search' => '(string) Limit response to include only tags whose names or slugs match the provided search query.',
'order' => array(
'ASC' => 'Return tags in ascending order.',
'DESC' => 'Return tags in descending order.',
),
'order_by' => array(
'name' => 'Order by the name of each tag.',
'count' => 'Order by the number of posts in each tag.',
),
),
'allow_fallback_to_jetpack_blog_token' => true,
'response_format' => array(
'found' => '(int) The number of tags returned.',
'tags' => '(array) Array of tag objects.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/?number=5',
)
);
/**
* GET taxonomies endpoint class.
*/
class WPCOM_JSON_API_Get_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
* /sites/%s/tags -> $blog_id
* /sites/%s/categories -> $blog_id
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$args = $this->process_args( $args );
if ( preg_match( '#/tags#i', $path ) ) {
return $this->tags( $args );
} else {
return $this->categories( $args );
}
}
/**
* Process args.
*
* @param array $args - the arguments.
*/
public function process_args( $args ) {
if ( $args['number'] < 1 ) {
$args['number'] = 100;
} elseif ( 1000 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 1000.', 400 );
}
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$args['offset'] = ( $args['page'] - 1 ) * $args['number'];
unset( $args['page'] );
}
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$args['orderby'] = $args['order_by'];
unset( $args['order_by'] );
unset( $args['context'], $args['pretty'], $args['http_envelope'], $args['fields'] );
return $args;
}
/**
* Get categories.
*
* @param array $args - the arguments.
*/
public function categories( $args ) {
$args['get'] = 'all';
$cats = get_categories( $args );
unset( $args['offset'] );
$args['taxonomy'] = 'category';
$found = wp_count_terms( $args );
$cats_obj = array();
foreach ( $cats as $cat ) {
$cats_obj[] = $this->format_taxonomy( $cat, 'category', 'display' );
}
return array(
'found' => (int) $found,
'categories' => $cats_obj,
);
}
/**
* Get tags.
*
* @param array $args - the arguments.
*/
public function tags( $args ) {
$args['get'] = 'all';
$tags = (array) get_tags( $args );
unset( $args['offset'] );
$args['taxonomy'] = 'post_tag';
$found = wp_count_terms( $args );
$tags_obj = array();
foreach ( $tags as $tag ) {
$tags_obj[] = $this->format_taxonomy( $tag, 'post_tag', 'display' );
}
return array(
'found' => (int) $found,
'tags' => $tags_obj,
);
}
}
@@ -0,0 +1,79 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Taxonomy_Endpoint(
array(
'description' => 'Get information about a single category.',
'group' => 'taxonomy',
'stat' => 'categories:1',
'method' => 'GET',
'path' => '/sites/%s/categories/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$category' => '(string) The category slug',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/categories/slug:community',
)
);
new WPCOM_JSON_API_Get_Taxonomy_Endpoint(
array(
'description' => 'Get information about a single tag.',
'group' => 'taxonomy',
'stat' => 'tags:1',
'method' => 'GET',
'path' => '/sites/%s/tags/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$tag' => '(string) The tag slug',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/slug:wordpresscom',
)
);
/**
* GET Taxonomy endpoint class.
*/
class WPCOM_JSON_API_Get_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
/**
*
* API callback.
*
* /sites/%s/tags/slug:%s -> $blog_id, $tag_id
* /sites/%s/categories/slug:%s -> $blog_id, $tag_id
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $taxonomy_id - the taxonomy ID.
*/
public function callback( $path = '', $blog_id = 0, $taxonomy_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
if ( preg_match( '#/tags/#i', $path ) ) {
$taxonomy_type = 'post_tag';
} else {
$taxonomy_type = 'category';
}
$return = $this->get_taxonomy( $taxonomy_id, $taxonomy_type, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'taxonomies' );
return $return;
}
}
@@ -0,0 +1,72 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Get_Term_Endpoint(
array(
'description' => 'Get information about a single term.',
'group' => 'taxonomy',
'stat' => 'terms:1',
'method' => 'GET',
'path' => '/sites/%s/taxonomies/%s/terms/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$taxonomy' => '(string) Taxonomy',
'$slug' => '(string) Term slug',
),
'response_format' => array(
'ID' => '(int) The term ID.',
'name' => '(string) The name of the term.',
'slug' => '(string) The slug of the term.',
'description' => '(string) The description of the term.',
'post_count' => '(int) The number of posts using this term.',
'parent' => '(int) The parent ID for the term, if hierarchical.',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/taxonomies/post_tag/terms/slug:wordpresscom',
)
);
/**
* GET Term endpoint class.
*/
class WPCOM_JSON_API_Get_Term_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
*
* API callback.
*
* /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param string $taxonomy - the taxonomy type.
* @param int $slug - the slug.
*/
public function callback( $path = '', $blog_id = 0, $taxonomy = 'category', $slug = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
$taxonomy_meta = get_taxonomy( $taxonomy );
if ( false === $taxonomy_meta || ( ! $taxonomy_meta->public &&
! current_user_can( $taxonomy_meta->cap->assign_terms ) ) ) {
return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 );
}
$args = $this->query_args();
$term = $this->get_taxonomy( $slug, $taxonomy, $args['context'] );
if ( ! $term || is_wp_error( $term ) ) {
return $term;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'terms' );
return $term;
}
}
@@ -0,0 +1,380 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Comments Walker Class.
*/
class WPCOM_JSON_API_List_Comments_Walker extends Walker {
/**
* Tree type.
*
* @var string
*/
public $tree_type = 'comment';
/**
* Database fields.
*
* @var array
*/
public $db_fields = array(
'parent' => 'comment_parent',
'id' => 'comment_ID',
);
/**
* Start the element output.
*
* @param array $output - the output.
* @param object $object - the object.
* @param int $depth - depth.
* @param array $args - the arguments.
* @param int $current_object_id - the object ID.
*/
public function start_el( &$output, $object, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! is_array( $output ) ) {
$output = array();
}
$output[] = $object->comment_ID;
}
/**
* Taken from WordPress's Walker_Comment::display_element()
*
* This function is designed to enhance Walker::display_element() to
* display children of higher nesting levels than selected inline on
* the highest depth level displayed. This prevents them being orphaned
* at the end of the comment list.
*
* Example: max_depth = 2, with 5 levels of nested content.
* 1
* 1.1
* 1.1.1
* 1.1.1.1
* 1.1.1.1.1
* 1.1.2
* 1.1.2.1
* 2
* 2.2
*
* @see Walker_Comment::display_element()
* @see Walker::display_element()
* @see wp_list_comments()
*
* @param object $element — Data object.
* @param array $children_elements - List of elements to continue traversing (passed by reference).
* @param int $max_depth — Max depth to traverse.
* @param int $depth — Depth of current element.
* @param array $args — An array of arguments.
* @param string $output — Used to append additional content (passed by reference).
*/
public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
if ( ! $element ) {
return;
}
$id_field = $this->db_fields['id'];
$id = $element->$id_field;
parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
// If we're at the max depth, and the current element still has children, loop over those and display them at this level
// This is to prevent them being orphaned to the end of the list.
if ( $max_depth <= $depth + 1 && isset( $children_elements[ $id ] ) ) {
foreach ( $children_elements[ $id ] as $child ) {
$this->display_element( $child, $children_elements, $max_depth, $depth, $args, $output );
}
unset( $children_elements[ $id ] );
}
}
}
new WPCOM_JSON_API_List_Comments_Endpoint(
array(
'description' => 'Get a list of recent comments.',
'group' => 'comments',
'stat' => 'comments',
'method' => 'GET',
'path' => '/sites/%s/comments/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/?number=2',
)
);
new WPCOM_JSON_API_List_Comments_Endpoint(
array(
'description' => 'Get a list of recent comments on a post.',
'group' => 'comments',
'stat' => 'posts:1:replies',
'method' => 'GET',
'path' => '/sites/%s/posts/%d/replies/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/replies/?number=2',
)
);
/**
* List comment endpoint.
*
* /sites/%s/comments/ -> $blog_id
* /sites/%s/posts/%d/replies/ -> $blog_id, $post_id
* /sites/%s/comments/%d/replies/ -> $blog_id, $comment_id
*
* @todo permissions
*/
class WPCOM_JSON_API_List_Comments_Endpoint extends WPCOM_JSON_API_Comment_Endpoint { // phpcs:ignore
/**
* The response format.
*
* @var array
*/
public $response_format = array(
'found' => '(int) The total number of comments found that match the request (ignoring limits, offsets, and pagination).',
'site_ID' => '(int) The site ID',
'comments' => '(array:comment) An array of comment objects.',
);
/**
* Constructor function.
*
* @param array $args - the arguments.
*/
public function __construct( $args ) {
parent::__construct( $args );
$this->query = array_merge(
$this->query,
array(
'number' => '(int=20) The number of comments to return. Limit: 100. When using hierarchical=1, number refers to the number of top-level comments returned.',
'offset' => '(int=0) 0-indexed offset. Not available if using hierarchical=1.',
'page' => '(int) Return the Nth 1-indexed page of comments. Takes precedence over the <code>offset</code> parameter. When using hierarchical=1, pagination is a bit different. See the note on the number parameter.',
'order' => array(
'DESC' => 'Return comments in descending order from newest to oldest.',
'ASC' => 'Return comments in ascending order from oldest to newest.',
),
'hierarchical' => array(
'false' => '',
'true' => '(BETA) Order the comment list hierarchically.',
),
'after' => '(ISO 8601 datetime) Return comments dated on or after the specified datetime. Not available if using hierarchical=1.',
'before' => '(ISO 8601 datetime) Return comments dated on or before the specified datetime. Not available if using hierarchical=1.',
'type' => array(
'any' => 'Return all comments regardless of type.',
'comment' => 'Return only regular comments.',
'trackback' => 'Return only trackbacks.',
'pingback' => 'Return only pingbacks.',
'pings' => 'Return both trackbacks and pingbacks.',
),
'status' => array(
'approved' => 'Return only approved comments.',
'unapproved' => 'Return only comments in the moderation queue.',
'spam' => 'Return only comments marked as spam.',
'trash' => 'Return only comments in the trash.',
'all' => 'Return comments of all statuses.',
),
'author_wpcom_data' => array(
'false' => 'Do not add wpcom_id and wpcom_login fields to comment author responses (default)',
'true' => 'Add wpcom_id and wpcom_login fields to comment author responses',
),
)
);
}
/**
* The callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $object_id - the object ID.
*/
public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
if ( str_contains( $path, '/posts/' ) ) {
// We're looking for comments of a particular post.
$post_id = $object_id;
$comment_id = 0;
} else {
// We're looking for comments for the whole blog, or replies to a single comment.
$comment_id = $object_id;
$post_id = 0;
}
// We can't efficiently get the number of replies to a single comment.
$count = false;
$found = -1;
if ( ! $comment_id ) {
// We can get comment counts for the whole site or for a single post, but only for certain queries.
if ( 'any' === $args['type'] && ! isset( $args['after'] ) && ! isset( $args['before'] ) ) {
$count = $this->api->wp_count_comments( $post_id );
}
}
switch ( $args['status'] ) {
case 'approved':
$status = 'approve';
if ( $count ) {
$found = $count->approved;
}
break;
default:
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'User cannot read non-approved comments', 403 );
}
if ( 'unapproved' === $args['status'] ) {
$status = 'hold';
$count_status = 'moderated';
} elseif ( 'all' === $args['status'] ) {
$status = 'all';
$count_status = 'total_comments';
} else {
$status = $args['status'];
$count_status = $args['status'];
}
if ( $count ) {
$found = $count->$count_status;
}
}
/** This filter is documented in class.json-api.php */
$exclude = apply_filters(
'jetpack_api_exclude_comment_types',
array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
);
$query = array(
'order' => $args['order'],
'type' => 'any' === $args['type'] ? false : $args['type'],
'status' => $status,
'type__not_in' => $exclude,
);
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
} elseif ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
if ( ! $args['hierarchical'] ) {
$query['number'] = $args['number'];
if ( isset( $args['page'] ) ) {
$query['offset'] = ( $args['page'] - 1 ) * $args['number'];
} else {
$query['offset'] = $args['offset'];
}
$is_before = isset( $args['before_gmt'] );
$is_after = isset( $args['after_gmt'] );
if ( $is_before || $is_after ) {
$query['date_query'] = array(
'column' => 'comment_date_gmt',
'inclusive' => true,
);
if ( $is_before ) {
$query['date_query']['before'] = $args['before_gmt'];
}
if ( $is_after ) {
$query['date_query']['after'] = $args['after_gmt'];
}
}
}
if ( $args['hierarchical'] && $found > 5000 ) {
// Massive comment thread found; don't pre-load comment metadata to reduce memory used.
$query['update_comment_meta_cache'] = false;
}
if ( $post_id ) {
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
$query['post_id'] = $post->ID;
if ( $this->api->ends_with( $this->path, '/replies' ) ) {
$query['parent'] = 0;
}
} elseif ( $comment_id ) {
$comment = get_comment( $comment_id );
if ( ! $comment || is_wp_error( $comment ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
$query['parent'] = $comment_id;
}
$comments = get_comments( $query );
if ( $args['hierarchical'] ) {
$walker = new WPCOM_JSON_API_List_Comments_Walker();
$comment_ids = $walker->paged_walk( $comments, get_option( 'thread_comments_depth', -1 ), isset( $args['page'] ) ? $args['page'] : 1, $args['number'] );
if ( ! empty( $comment_ids ) ) {
$comments = array_map( 'get_comment', $comment_ids );
}
}
$return = array();
foreach ( array_keys( $this->response_format ) as $key ) {
switch ( $key ) {
case 'found':
$return[ $key ] = (int) $found;
break;
case 'site_ID':
$return[ $key ] = (int) $blog_id;
break;
case 'comments':
$return_comments = array();
if ( ! empty( $comments ) ) {
foreach ( $comments as $comment ) {
$the_comment = $this->get_comment( $comment->comment_ID, $args['context'] );
if ( $the_comment && ! is_wp_error( $the_comment ) ) {
$return_comments[] = $the_comment;
}
}
}
if ( $return_comments ) {
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'comments', count( $return_comments ) );
}
$return[ $key ] = $return_comments;
break;
}
}
return $return;
}
}
@@ -0,0 +1,219 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List dropdown pages endpoint.
*/
new WPCOM_JSON_API_List_Dropdown_Pages_Endpoint(
array(
'description' => 'Get a list of pages to be displayed as options in a select-a-page-dropdown.',
'min_version' => '1.1',
'max_version' => '1.1',
'group' => 'posts',
'stat' => 'posts:dropdown-pages',
'method' => 'GET',
'path' => '/sites/%s/dropdown-pages/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/dropdown-pages/',
)
);
/**
* Endpoint class responsible for listing pages to be displayed as options in a select-a-page-dropdown.
*
* /sites/%s/dropdown-pages/ -> $blog_id
*/
class WPCOM_JSON_API_List_Dropdown_Pages_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Page object format.
*
* @var array
*/
public $dropdown_page_object_format = array(
'ID' => '(int) The page ID.',
'title' => '(string) The page title.',
'children' => '(array:dropdown_page) An array of child pages.',
);
/**
* The response format.
*
* @var array
*/
public $response_format = array(
'found' => '(int) The number of pages found.',
'dropdown_pages' => '(array:dropdown_page) An array of dropdown_page objects.',
);
/**
* List of pages indexed by their page ID.
*
* @var array<int,WP_Post>
*/
private $pages_by_id = array();
/**
* List of pages indexed by their parent page ID.
*
* @var array<int,WP_Post>
*/
private $pages_by_parent = array();
/**
* API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @return stdClass[] $pages - An array of page objects. Each page object includes ID and title properties and may include children property. This makes each page object a tree-like data structure.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$pages = get_pages();
if ( empty( $pages ) ) {
return array(
'found' => 0,
'dropdown_pages' => array(),
);
}
$this->pages_by_id = self::to_pages_by_id( $pages );
$this->pages_by_parent = self::to_pages_by_parent( $pages );
$dropdown_pages = $this->create_dropdown_pages();
return array(
'found' => count( $dropdown_pages ),
'dropdown_pages' => $dropdown_pages,
);
}
/**
* Convert a list of pages to a list of pages by page ID.
*
* @param array<WP_Post> $pages - array of pages.
* @return array<int,WP_Post> $pages_by_page_id - indexed array of pages by page ID where index is page ID.
*/
private static function to_pages_by_id( $pages ) {
$pages_by_page_id = array();
foreach ( $pages as $page ) {
if ( isset( $page->ID ) ) {
$pages_by_page_id[ $page->ID ] = $page;
}
}
return $pages_by_page_id;
}
/**
* Convert a list of pages to a list of pages by parent.
*
* @param array<WP_Post> $pages - array of pages.
* @return array<int,WP_Post> $pages_by_parent - indexed array of pages by parent where index is page ID.
*/
private static function to_pages_by_parent( $pages ) {
$pages_by_parent = array();
foreach ( $pages as $page ) {
if ( empty( $page->post_parent ) ) {
$pages_by_parent['root'][] = $page;
} else {
$pages_by_parent[ $page->post_parent ][] = $page;
}
}
return $pages_by_parent;
}
/**
* Convert a list of pages to a list of dropdown pages.
*
* @return array<stdClass> $dropdown_pages - array of dropdown pages.
*/
private function create_dropdown_pages() {
$dropdown_pages = array();
if ( ! empty( $this->pages_by_parent['root'] ) ) {
foreach ( $this->pages_by_parent['root'] as $root_page ) {
$dropdown_pages[] = $this->to_dropdown_page( $root_page );
}
}
if ( ! empty( $this->pages_by_id ) ) {
// In case there were some orphans
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
foreach ( $this->pages_by_id as $_page_id => $page ) {
$dropdown_pages[] = $this->to_dropdown_page( $page );
}
}
return $dropdown_pages;
}
/**
* Convert a page to a dropdown page.
*
* @param WP_Post $page - the page.
* @return stdClass|false $dropdown_page - the dropdown page.
*/
private function to_dropdown_page( $page ) {
if ( ! isset( $page->ID ) ) {
return false;
}
$title = $this->get_page_title( $page );
if ( ! isset( $this->pages_by_parent[ $page->ID ] ) ) {
unset( $this->pages_by_id[ $page->ID ] );
return (object) array(
'ID' => $page->ID,
'title' => $title,
);
}
$children = array();
foreach ( $this->pages_by_parent[ $page->ID ] as $child_page ) {
$children[] = $this->to_dropdown_page( $child_page );
}
unset( $this->pages_by_id[ $page->ID ] );
unset( $this->pages_by_parent[ $page->ID ] );
return (object) array(
'ID' => $page->ID,
'title' => $title,
'children' => $children,
);
}
/**
* Get the page title.
*
* @param WP_Post $page - the page.
* @return string $page_title - the page title.
*/
private function get_page_title( $page ) {
$title = $page->post_title;
if ( '' === $title ) {
/* translators: %d: ID of a post. */
$title = sprintf( __( '#%d (no title)', 'jetpack' ), $page->ID );
}
/**
* Filters the page title when creating an HTML drop-down list of pages.
*
* @since 3.1.0
*
* @param string $title Page title.
* @param WP_Post $page Page data object.
*/
$title = apply_filters( 'list_pages', $title, $page );
return $title;
}
}
@@ -0,0 +1,77 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List Embeds endpoint.
*/
new WPCOM_JSON_API_List_Embeds_Endpoint(
array(
'description' => 'Get a list of embeds available on a site. Note: The current user must have publishing access.',
'group' => 'sites',
'stat' => 'embeds',
'method' => 'GET',
'path' => '/sites/%s/embeds',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'response_format' => array(
'embeds' => '(array) A list of supported embeds by their regex pattern.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/embeds',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List Embeds Endpoint class.
*
* /sites/%s/embeds -> $blog_id
*/
class WPCOM_JSON_API_List_Embeds_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// permissions check.
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 );
}
// list em.
$output = array( 'embeds' => array() );
if ( ! function_exists( '_wp_oembed_get_object' ) ) {
require_once ABSPATH . WPINC . '/class-oembed.php';
}
global $wp_embed;
$oembed = _wp_oembed_get_object();
foreach ( $wp_embed->handlers as $handlers ) {
foreach ( $handlers as $handler ) {
if ( ! empty( $handler['regex'] ) ) {
$output['embeds'][] = $handler['regex'];
}
}
}
foreach ( $oembed->providers as $regex => $oembed_info ) {
if ( ! empty( $regex ) ) {
$output['embeds'][] = $regex;
}
}
return $output;
}
}
@@ -0,0 +1,97 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List media endpoint.
*/
new WPCOM_JSON_API_List_Media_Endpoint(
array(
'description' => 'Get a list of items in the media library.',
'group' => 'media',
'stat' => 'media',
'method' => 'GET',
'path' => '/sites/%s/media/',
'deprecated' => true,
'new_version' => '1.1',
'max_version' => '1',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=20) The number of media items to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'parent_id' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.',
'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).",
),
'response_format' => array(
'media' => '(array) Array of media',
'found' => '(int) The number of total results found',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/?number=2',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* API List media endpoint class.
*/
class WPCOM_JSON_API_List_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// upload_files can probably be used for other endpoints but we want contributors to be able to use media too.
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$args = $this->query_args();
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
$media = get_posts(
array(
'post_type' => 'attachment',
'post_parent' => $args['parent_id'],
'offset' => $args['offset'],
'numberposts' => $args['number'],
'post_mime_type' => $args['mime_type'],
)
);
$response = array();
foreach ( $media as $item ) {
$response[] = $this->get_media_item( $item->ID );
}
$_num = (array) wp_count_attachments();
$_total_media = array_sum( $_num ) - $_num['trash'];
$return = array(
'found' => $_total_media,
'media' => $response,
);
return $return;
}
}
@@ -0,0 +1,351 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List Media v1_1 endpoint.
*/
new WPCOM_JSON_API_List_Media_v1_1_Endpoint(
array(
'description' => 'Get a list of items in the media library.',
'group' => 'media',
'stat' => 'media',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'GET',
'path' => '/sites/%s/media/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=20) The number of media items to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.',
'order' => array(
'DESC' => 'Return files in descending order. For dates, that means newest to oldest.',
'ASC' => 'Return files in ascending order. For dates, that means oldest to newest.',
),
'order_by' => array(
'date' => 'Order by the uploaded time of each file.',
'title' => 'Order lexicographically by file titles.',
'ID' => 'Order by media ID.',
),
'search' => '(string) Search query.',
'post_ID' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.',
'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).",
'after' => '(ISO 8601 datetime) Return media items uploaded after the specified datetime.',
'before' => '(ISO 8601 datetime) Return media items uploaded before the specified datetime.',
),
'response_format' => array(
'media' => '(array) Array of media objects',
'found' => '(int) The number of total results found',
'meta' => '(object) Meta data',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List media v1_1 endpoint class.
*/
class WPCOM_JSON_API_List_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore
/**
* Date range.
*
* @var array
*/
public $date_range = array();
/**
* The page handle.
*
* @var array
*/
public $page_handle = array();
/**
* Performed query
*
* @var array
*/
public $performed_query = array();
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// upload_files can probably be used for other endpoints but we want contributors to be able to use media too.
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$args = $this->query_args();
$is_eligible_for_page_handle = true;
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
if ( isset( $args['before'] ) ) {
$this->date_range['before'] = $args['before'];
}
if ( isset( $args['after'] ) ) {
$this->date_range['after'] = $args['after'];
}
$query = array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_parent' => isset( $args['post_ID'] ) ? $args['post_ID'] : null,
'posts_per_page' => $args['number'],
'post_mime_type' => isset( $args['mime_type'] ) ? $args['mime_type'] : null,
'order' => isset( $args['order'] ) ? $args['order'] : 'DESC',
'orderby' => isset( $args['order_by'] ) ? $args['order_by'] : 'date',
's' => isset( $args['search'] ) ? $args['search'] : null,
'meta_query' => array(
array(
'key' => 'videopress_poster_image',
'compare' => 'NOT EXISTS',
),
),
);
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$query['paged'] = $args['page'];
if ( 1 !== $query['paged'] ) {
$is_eligible_for_page_handle = false;
}
} else {
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$query['offset'] = $args['offset'];
if ( 0 !== $query['offset'] ) {
$is_eligible_for_page_handle = false;
}
}
if ( isset( $args['page_handle'] ) ) {
$page_handle = wp_parse_args( $args['page_handle'] );
if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) {
// we have a valid looking page handle.
$this->page_handle = $page_handle;
add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
}
if ( $this->date_range ) {
add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
}
$this->performed_query = $query;
add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
$media = new WP_Query( $query );
remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
if ( $this->date_range ) {
remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
$this->date_range = array();
}
if ( $this->page_handle ) {
remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
$response = array();
foreach ( $media->posts as $item ) {
$response[] = $this->get_media_item_v1_1( $item->ID );
}
$return = array(
'found' => (int) $media->found_posts,
'media' => $response,
);
if ( $is_eligible_for_page_handle && $return['media'] ) {
$last_post = end( $return['media'] );
reset( $return['media'] );
if ( ( $return['found'] > count( $return['media'] ) ) && $last_post ) {
$return['meta'] = array();
$return['meta']['next_page'] = $this->build_page_handle( $last_post, $query );
}
}
return $return;
}
/**
* Build the page handle.
*
* @param object $post - the post object.
* @param array $query - the query.
*/
public function build_page_handle( $post, $query ) {
$column = $query['orderby'];
if ( ! $column ) {
$column = 'date';
}
return build_query(
array(
'value' => rawurlencode( $post->$column ),
'id' => $post->ID,
)
);
}
/**
* Handle figuring out the page handler is.
*
* @param string $where - sql where clause.
*/
public function handle_where_for_page_handle( $where ) {
global $wpdb;
$column = $this->performed_query['orderby'];
if ( ! $column ) {
$column = 'date';
}
$order = $this->performed_query['order'];
if ( ! $order ) {
$order = 'DESC';
}
if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ), true ) ) {
return $where;
}
if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) {
return $where;
}
$db_column = '';
$db_value = '';
switch ( $column ) {
case 'ID':
$db_column = 'ID';
$db_value = '%d';
break;
case 'title':
$db_column = 'post_title';
$db_value = '%s';
break;
case 'date':
$db_column = 'post_date';
$db_value = 'CAST( %s as DATETIME )';
break;
case 'modified':
$db_column = 'post_modified';
$db_value = 'CAST( %s as DATETIME )';
break;
case 'comment_count':
$db_column = 'comment_count';
$db_value = '%d';
break;
}
if ( 'DESC' === $order ) {
$db_order = '<';
} else {
$db_order = '>';
}
// Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item
// but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items
// beyond the passed item.
$where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
if ( 'ID' !== $db_column ) {
$where .= $wpdb->prepare( "OR ( `$wpdb->posts`.`$db_column` = $db_value AND `$wpdb->posts`.ID $db_order %d )", $this->page_handle['value'], $this->page_handle['id'] ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
$where .= ' )';
return $where;
}
/**
* Handle date range.
*
* @param string $where - sql where clause.
*/
public function handle_date_range( $where ) {
global $wpdb;
switch ( count( $this->date_range ) ) {
case 2:
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ",
$this->date_range['after'],
$this->date_range['before']
);
break;
case 1:
if ( isset( $this->date_range['before'] ) ) {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date <= CAST( %s AS DATETIME ) ",
$this->date_range['before']
);
} else {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date >= CAST( %s AS DATETIME ) ",
$this->date_range['after']
);
}
break;
}
return $where;
}
/**
* Handle how page handle is ordered by.
*
* @param string $orderby - how we want to order things by.
*/
public function handle_orderby_for_page_handle( $orderby ) {
global $wpdb;
if ( 'ID' === $this->performed_query['orderby'] ) {
// bail if we're already ordering by ID.
return $orderby;
}
if ( $orderby ) {
$orderby .= ' ,';
}
$order = $this->performed_query['order'];
if ( ! $order ) {
$order = 'DESC';
}
$orderby .= " `$wpdb->posts`.ID $order";
return $orderby;
}
}
@@ -0,0 +1,90 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php';
/**
* List media v1_2 endpoint.
*/
new WPCOM_JSON_API_List_Media_v1_2_Endpoint(
array(
'description' => 'Get a list of items in the media library.',
'group' => 'media',
'stat' => 'media',
'min_version' => '1.2',
'max_version' => '1.2',
'method' => 'GET',
'path' => '/sites/%s/media/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=20) The number of media items to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.',
'order' => array(
'DESC' => 'Return files in descending order. For dates, that means newest to oldest.',
'ASC' => 'Return files in ascending order. For dates, that means oldest to newest.',
),
'order_by' => array(
'date' => 'Order by the uploaded time of each file.',
'title' => 'Order lexicographically by file titles.',
'ID' => 'Order by media ID.',
),
'search' => '(string) Search query.',
'post_ID' => '(int) Default is showing all items. The post where the media item is attached. 0 shows unattached media items.',
'mime_type' => "(string) Default is empty. Filter by mime type (e.g., 'image/jpeg', 'application/pdf'). Partial searches also work (e.g. passing 'image' will search for all image files).",
'after' => '(ISO 8601 datetime) Return media items uploaded after the specified datetime.',
'before' => '(ISO 8601 datetime) Return media items uploaded before the specified datetime.',
),
'response_format' => array(
'media' => '(array) Array of media objects',
'found' => '(int) The number of total results found',
'meta' => '(object) Meta data',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List Media v1_2 endpoint.
*/
class WPCOM_JSON_API_List_Media_v1_2_Endpoint extends WPCOM_JSON_API_List_Media_v1_1_Endpoint { // phpcs:ignore
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$response = parent::callback( $path, $blog_id );
if ( is_wp_error( $response ) ) {
return $response;
}
$media_list = $response['media'];
if ( ! is_countable( $media_list ) || count( $media_list ) === array() ) {
return $response;
}
foreach ( $media_list as $media_item ) {
// expose `revision_history` object for each image.
$media_item->revision_history = (object) array(
'items' => (array) Jetpack_Media::get_revision_history( $media_item->ID ),
'original' => (object) Jetpack_Media::get_original_media( $media_item->ID ),
);
}
return $response;
}
}
@@ -0,0 +1,124 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List post type taxonomies endpoint.
*/
new WPCOM_JSON_API_List_Post_Type_Taxonomies_Endpoint(
array(
'description' => 'Get a list of taxonomies associated with a post type.',
'group' => 'taxonomy',
'stat' => 'sites:X:post-types:X:taxonomies',
'method' => 'GET',
'path' => '/sites/%s/post-types/%s/taxonomies',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_type' => '(string) Post type',
),
'response_format' => array(
'found' => '(int) The number of taxonomies found',
'taxonomies' => '(array:taxonomy) A list of available taxonomies',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/33534099/post-types/post/taxonomies',
)
);
/**
* List post type taxonomies endpoint class.
*
* /sites/%s/post-types/%s/taxonomies -> $blog_id, $post_type
*/
class WPCOM_JSON_API_List_Post_Type_Taxonomies_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Included taxonomy keys.
*
* @var array
*/
public static $taxonomy_keys_to_include = array(
'name' => 'name',
'label' => 'label',
'labels' => 'labels',
'description' => 'description',
'hierarchical' => 'hierarchical',
'public' => 'public',
'cap' => 'capabilities',
);
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
* @param string $post_type - the post type.
*/
public function callback( $path = '', $blog_id = 0, $post_type = 'post' ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
$this->localize_initial_taxonomies( $post_type );
$post_type_object = get_post_type_object( $post_type );
if ( ! $post_type_object || ( ! $post_type_object->publicly_queryable && (
! current_user_can( $post_type_object->cap->edit_posts ) ) ) ) {
return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
}
// Get a list of available taxonomies.
$taxonomy_objects = get_object_taxonomies( $post_type, 'objects' );
// Construct array of formatted objects.
$formatted_taxonomy_objects = array();
foreach ( $taxonomy_objects as $taxonomy_object ) {
// Omit private taxonomies unless user has assign capability.
if ( ! $taxonomy_object->public && ! current_user_can( $taxonomy_object->cap->assign_terms ) ) {
continue;
}
// Include only the desired keys in the response.
$formatted_taxonomy_object = array();
foreach ( self::$taxonomy_keys_to_include as $key => $value ) {
$formatted_taxonomy_object[ $value ] = $taxonomy_object->{ $key };
}
$formatted_taxonomy_objects[] = $formatted_taxonomy_object;
}
return array(
'found' => count( $formatted_taxonomy_objects ),
'taxonomies' => $formatted_taxonomy_objects,
);
}
/**
* Handle localizing initial taxonomies.
*
* @param string $post_type - the post type.
*/
protected function localize_initial_taxonomies( $post_type ) {
/** This filter is documented in jetpack/json-endpoints/class.wpcom-json-api-list-post-types-endpoint.php */
if ( ! apply_filters( 'rest_api_localize_response', false ) ) {
return;
}
// Since recreating initial taxonomies will restore the default post
// types to which they are associated, save post type's taxonomies in
// case it was customized via `register_taxonomy_for_object_type`.
$post_type_taxonomies = get_object_taxonomies( $post_type );
// API localization occurs after the initial taxonomies have been
// registered, so re-register if localizing response.
create_initial_taxonomies();
// Restore registered taxonomies for post type.
foreach ( $post_type_taxonomies as $taxonomy ) {
register_taxonomy_for_object_type( $taxonomy, $post_type );
}
}
}
@@ -0,0 +1,152 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List post types endpoint.
*/
new WPCOM_JSON_API_List_Post_Types_Endpoint(
array(
'description' => 'Get a list of post types available for a site.',
'group' => 'sites',
'stat' => 'sites:X:post-types',
'method' => 'GET',
'path' => '/sites/%s/post-types',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'query_parameters' => array(
'api_queryable' => '(bool) If true, only queryable post types are returned',
),
'response_format' => array(
'found' => '(int) The number of post types found',
'post_types' => '(array) A list of available post types',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/33534099/post-types',
)
);
/**
* List Post types endpoint class.
*
* /sites/%s/post-types -> $blog_id
*/
class WPCOM_JSON_API_List_Post_Types_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Post type keys to include.
*
* @var array
*/
public static $post_type_keys_to_include = array(
'name' => 'name',
'label' => 'label',
'labels' => 'labels',
'description' => 'description',
'map_meta_cap' => 'map_meta_cap',
'cap' => 'capabilities',
'hierarchical' => 'hierarchical',
'public' => 'public',
'show_ui' => 'show_ui',
'publicly_queryable' => 'publicly_queryable',
);
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
/**
* Whether API responses should be returned in a custom locale. False
* for Jetpack; may be true for WP.com requests.
*
* @since 3.9.2
*/
if ( apply_filters( 'rest_api_localize_response', false ) ) {
// API localization occurs after the initial post types have been
// registered, so let's get the post type labels translated.
if ( 'en' !== get_locale() ) {
global $wp_post_types;
foreach ( $wp_post_types as $post_type_object ) {
foreach ( array_keys( (array) $post_type_object->labels ) as $label_key ) {
// Direct use of translate call because this doesn't need to be extracted.
// phpcs:ignore WordPress.WP.I18n
$post_type_object->labels->$label_key = translate( $post_type_object->labels->$label_key, 'default' );
}
}
}
}
}
// Get a list of available post types.
$post_types = get_post_types();
$formatted_post_type_objects = array();
// Retrieve post type object for each post type.
foreach ( $post_types as $post_type ) {
// Skip non-queryable if filtering on queryable only.
$is_queryable = $this->is_post_type_allowed( $post_type );
if ( ! $is_queryable ) {
continue;
}
$post_type_object = get_post_type_object( $post_type );
$formatted_post_type_object = array();
// Include only the desired keys in the response.
foreach ( self::$post_type_keys_to_include as $key => $value ) {
$formatted_post_type_object[ $value ] = $post_type_object->{ $key };
}
$formatted_post_type_object['api_queryable'] = $is_queryable;
$formatted_post_type_object['supports'] = get_all_post_type_supports( $post_type );
if ( $this->post_type_supports_tags( $post_type ) ) {
$formatted_post_type_object['supports']['tags'] = true;
}
$formatted_post_type_objects[] = $formatted_post_type_object;
}
return array(
'found' => count( $formatted_post_type_objects ),
'post_types' => $formatted_post_type_objects,
);
}
/**
* See if post type supports tags.
*
* @param string $post_type - the post type.
*/
public function post_type_supports_tags( $post_type ) {
if ( in_array( 'post_tag', get_object_taxonomies( $post_type ), true ) ) {
return true;
}
// the featured content module adds post_tag support
// to the post types that are registered for it
// however it does so in a way that isn't available
// to get_object_taxonomies.
$featured_content = get_theme_support( 'featured-content' );
if ( ! $featured_content || empty( $featured_content[0] ) || empty( $featured_content[0]['post_types'] ) ) {
return false;
}
if ( is_array( $featured_content[0]['post_types'] ) ) {
return in_array( $post_type, $featured_content[0]['post_types'], true );
}
return $post_type === $featured_content[0]['post_types'];
}
}
@@ -0,0 +1,402 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List posts endpoint.
*/
new WPCOM_JSON_API_List_Posts_Endpoint(
array(
'description' => 'Get a list of matching posts.',
'new_version' => '1.1',
'max_version' => '1',
'group' => 'posts',
'stat' => 'posts',
'method' => 'GET',
'path' => '/sites/%s/posts/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'query_parameters' => array(
'number' => '(int=20) The number of posts to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
'order' => array(
'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.',
'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.',
),
'order_by' => array(
'date' => 'Order by the created time of each post.',
'modified' => 'Order by the modified time of each post.',
'title' => "Order lexicographically by the posts' titles.",
'comment_count' => 'Order by the number of comments for each post.',
'ID' => 'Order by post ID.',
),
'after' => '(ISO 8601 datetime) Return posts dated on or after the specified datetime.',
'before' => '(ISO 8601 datetime) Return posts dated on or before the specified datetime.',
'tag' => '(string) Specify the tag name or slug.',
'category' => '(string) Specify the category name or slug.',
'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.',
'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.',
'include' => '(array:int|int) Includes the specified post ID(s) in the response',
'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response',
'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.',
'status' => array(
'publish' => 'Return only published posts.',
'private' => 'Return only private posts.',
'draft' => 'Return only draft posts.',
'pending' => 'Return only posts pending editorial approval.',
'future' => 'Return only posts scheduled for future publishing.',
'trash' => 'Return only posts in the trash.',
'any' => 'Return all posts regardless of status.',
),
'sticky' => array(
'false' => 'Post is not marked as sticky.',
'true' => 'Stick the post to the front page.',
),
'author' => "(int) Author's user ID",
'search' => '(string) Search query',
'meta_key' => '(string) Metadata key that the post should contain',
'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/?number=5',
)
);
/**
* List posts endpoint class.
*
* /sites/%s/posts/ -> $blog_id
*/
class WPCOM_JSON_API_List_Posts_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
/**
* The date range.
*
* @var array
*/
public $date_range = array();
/**
* The response format.
*
* @var array
*/
public $response_format = array(
'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).',
'posts' => '(array:post) An array of post objects.',
);
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
if ( isset( $args['type'] ) && ! $this->is_post_type_allowed( $args['type'] ) ) {
return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
}
// Normalize post_type.
if ( isset( $args['type'] ) && 'any' === $args['type'] ) {
if ( version_compare( $this->api->version, '1.1', '<' ) ) {
$args['type'] = array( 'post', 'page' );
} else { // 1.1+
$args['type'] = $this->_get_whitelisted_post_types();
}
}
// determine statuses.
$status = $args['status'];
$status = ( $status ) ? explode( ',', $status ) : array( 'publish' );
if ( is_user_logged_in() ) {
$statuses_whitelist = array(
'publish',
'pending',
'draft',
'future',
'private',
'trash',
'any',
);
$status = array_intersect( $status, $statuses_whitelist );
} else {
// logged-out users can see only published posts.
$statuses_whitelist = array( 'publish', 'any' );
$status = array_intersect( $status, $statuses_whitelist );
if ( empty( $status ) ) {
// requested only protected statuses? nothing for you here.
return array(
'found' => 0,
'posts' => array(),
);
}
// clear it (AKA published only) because "any" includes protected.
$status = array();
}
// let's be explicit about defaulting to 'post'.
$args['type'] = isset( $args['type'] ) ? $args['type'] : 'post';
// make sure the user can read or edit the requested post type(s).
if ( is_array( $args['type'] ) ) {
$allowed_types = array();
foreach ( $args['type'] as $post_type ) {
if ( $this->current_user_can_access_post_type( $post_type, $args['context'] ) ) {
$allowed_types[] = $post_type;
}
}
if ( empty( $allowed_types ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
$args['type'] = $allowed_types;
} elseif ( ! $this->current_user_can_access_post_type( $args['type'], $args['context'] ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
$query = array(
'posts_per_page' => $args['number'],
'order' => $args['order'],
'orderby' => $args['order_by'],
'post_type' => $args['type'],
'post_status' => $status,
'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null,
'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,
's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null,
'fields' => 'ids',
);
if ( ! is_user_logged_in() ) {
$query['has_password'] = false;
}
if ( isset( $args['include'] ) ) {
$query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] );
}
if ( isset( $args['meta_key'] ) ) {
$show = false;
if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) {
$show = true;
}
if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) {
$show = true;
}
if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) {
return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 );
}
$meta = array( 'key' => $args['meta_key'] );
if ( isset( $args['meta_value'] ) ) {
$meta['value'] = $args['meta_value'];
}
$query['meta_query'] = array( $meta );
}
$sticky = get_option( 'sticky_posts' );
if (
isset( $args['sticky'] )
&&
$sticky
&&
is_array( $sticky )
) {
if ( $args['sticky'] ) {
$query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky;
} else {
$query['post__not_in'] = $sticky;
$query['ignore_sticky_posts'] = 1;
}
} else {
$query['post__not_in'] = $sticky;
$query['ignore_sticky_posts'] = 1;
}
if ( isset( $args['exclude'] ) ) {
$query['post__not_in'] = array_merge( $query['post__not_in'], (array) $args['exclude'] );
}
if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) {
// get_page_children is a misnomer; it supports all hierarchical post types.
$page_args = array(
'child_of' => $args['exclude_tree'],
'post_type' => $args['type'],
// since we're looking for things to exclude, be aggressive.
'post_status' => 'publish,draft,pending,private,future,trash',
);
$post_descendants = get_pages( $page_args );
$exclude_tree = array( $args['exclude_tree'] );
foreach ( $post_descendants as $child ) {
$exclude_tree[] = $child->ID;
}
$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree;
}
if ( isset( $args['category'] ) ) {
$category = get_term_by( 'slug', $args['category'], 'category' );
if ( false === $category ) {
$query['category_name'] = $args['category'];
} else {
$query['cat'] = $category->term_id;
}
}
if ( isset( $args['tag'] ) ) {
$query['tag'] = $args['tag'];
}
if ( ! empty( $args['term'] ) ) {
$query['tax_query'] = array();
foreach ( $args['term'] as $taxonomy => $slug ) {
$taxonomy_object = get_taxonomy( $taxonomy );
if ( false === $taxonomy_object || ( ! $taxonomy_object->public &&
! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) {
continue;
}
$query['tax_query'][] = array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => explode( ',', $slug ),
);
}
}
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$query['paged'] = $args['page'];
} else {
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$query['offset'] = $args['offset'];
}
if ( isset( $args['before'] ) ) {
$this->date_range['before'] = $args['before'];
}
if ( isset( $args['after'] ) ) {
$this->date_range['after'] = $args['after'];
}
if ( $this->date_range ) {
add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
}
/**
* 'column' necessary for the me/posts endpoint (which extends sites/$site/posts).
* Would need to be added to the sites/$site/posts definition if we ever want to
* use it there.
*/
$column_whitelist = array( 'post_modified_gmt' );
if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) {
$query['column'] = $args['column'];
}
$wp_query = new WP_Query( $query );
if ( $this->date_range ) {
remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
$this->date_range = array();
}
$return = array();
$excluded_count = 0;
foreach ( array_keys( $this->response_format ) as $key ) {
switch ( $key ) {
case 'found':
$return[ $key ] = (int) $wp_query->found_posts;
break;
case 'posts':
$posts = array();
foreach ( $wp_query->posts as $post_ID ) {
$the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] );
if ( $the_post && ! is_wp_error( $the_post ) ) {
$posts[] = $the_post;
} else {
++$excluded_count;
}
}
if ( $posts ) {
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) );
}
$return[ $key ] = $posts;
break;
}
}
$return['found'] -= $excluded_count;
return $return;
}
/**
* Handle the date range.
*
* @param string $where - SQL where clause.
*/
public function handle_date_range( $where ) {
global $wpdb;
switch ( count( $this->date_range ) ) {
case 2:
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ",
$this->date_range['after'],
$this->date_range['before']
);
break;
case 1:
if ( isset( $this->date_range['before'] ) ) {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date <= CAST( %s AS DATETIME ) ",
$this->date_range['before']
);
} else {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.post_date >= CAST( %s AS DATETIME ) ",
$this->date_range['after']
);
}
break;
}
return $where;
}
}
@@ -0,0 +1,637 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List posts v1_1 endpoint.
*/
new WPCOM_JSON_API_List_Posts_v1_1_Endpoint(
array(
'description' => 'Get a list of matching posts.',
'min_version' => '1.1',
'max_version' => '1.1',
'group' => 'posts',
'stat' => 'posts',
'method' => 'GET',
'path' => '/sites/%s/posts/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'query_parameters' => array(
'number' => '(int=20) The number of posts to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.',
'order' => array(
'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.',
'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.',
),
'order_by' => array(
'date' => 'Order by the created time of each post.',
'modified' => 'Order by the modified time of each post.',
'title' => "Order lexicographically by the posts' titles.",
'comment_count' => 'Order by the number of comments for each post.',
'ID' => 'Order by post ID.',
),
'after' => '(ISO 8601 datetime) Return posts dated after the specified datetime.',
'before' => '(ISO 8601 datetime) Return posts dated before the specified datetime.',
'modified_after' => '(ISO 8601 datetime) Return posts modified after the specified datetime.',
'modified_before' => '(ISO 8601 datetime) Return posts modified before the specified datetime.',
'tag' => '(string) Specify the tag name or slug.',
'category' => '(string) Specify the category name or slug.',
'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.',
'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.',
'include' => '(array:int|int) Includes the specified post ID(s) in the response',
'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response',
'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.',
'status' => '(string) Comma-separated list of statuses for which to query, including any of: "publish", "private", "draft", "pending", "future", and "trash", or simply "any". Defaults to "publish"',
'sticky' => array(
'include' => 'Sticky posts are not excluded from the list.',
'exclude' => 'Sticky posts are excluded from the list.',
'require' => 'Only include sticky posts',
),
'author' => "(int) Author's user ID",
'search' => '(string) Search query',
'meta_key' => '(string) Metadata key that the post should contain',
'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/?number=2',
)
);
/**
* List Posts v1_1 Endpoint class.
*
* /sites/%s/posts/ -> $blog_id
*/
class WPCOM_JSON_API_List_Posts_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { // phpcs:ignore
/**
* Date range
*
* @var array
*/
public $date_range = array();
/**
* Modified range
*
* @var array
*/
public $modified_range = array();
/**
* Page handle
*
* @var array
*/
public $page_handle = array();
/**
* Performed query
*
* @var array
*/
public $performed_query = null;
/**
* Response format.
*
* @var array
*/
public $response_format = array(
'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).',
'posts' => '(array:post) An array of post objects.',
'meta' => '(object) Meta data',
);
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$is_eligible_for_page_handle = true;
$site = $this->get_platform()->get_site( $blog_id );
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
if ( isset( $args['type'] ) &&
! in_array( $args['type'], array( 'post', 'revision', 'page', 'any' ), true ) &&
defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
if ( isset( $args['type'] ) && ! $site->is_post_type_allowed( $args['type'] ) ) {
return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
}
// Normalize post_type.
if ( isset( $args['type'] ) && 'any' === $args['type'] ) {
if ( version_compare( $this->api->version, '1.1', '<' ) ) {
$args['type'] = array( 'post', 'page' );
} else { // 1.1+
$args['type'] = $site->get_whitelisted_post_types();
}
}
// determine statuses.
$status = ( ! empty( $args['status'] ) ) ? explode( ',', $args['status'] ) : array( 'publish' );
if ( is_user_logged_in() ) {
$statuses_whitelist = array(
'publish',
'pending',
'draft',
'future',
'private',
'trash',
'any',
);
$status = array_intersect( $status, $statuses_whitelist );
} else {
// logged-out users can see only published posts.
$statuses_whitelist = array( 'publish', 'any' );
$status = array_intersect( $status, $statuses_whitelist );
if ( empty( $status ) ) {
// requested only protected statuses? nothing for you here.
return array(
'found' => 0,
'posts' => array(),
);
}
// clear it (AKA published only) because "any" includes protected.
$status = array();
}
// let's be explicit about defaulting to 'post'.
$args['type'] = isset( $args['type'] ) ? $args['type'] : 'post';
// make sure the user can read or edit the requested post type(s).
if ( is_array( $args['type'] ) ) {
$allowed_types = array();
foreach ( $args['type'] as $post_type ) {
if ( $site->current_user_can_access_post_type( $post_type, $args['context'] ) ) {
$allowed_types[] = $post_type;
}
}
if ( empty( $allowed_types ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
$args['type'] = $allowed_types;
} elseif ( ! $site->current_user_can_access_post_type( $args['type'], $args['context'] ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
$query = array(
'posts_per_page' => $args['number'],
'order' => $args['order'],
'orderby' => $args['order_by'],
'post_type' => $args['type'],
'post_status' => $status,
'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null,
'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,
's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null,
'fields' => 'ids',
);
if ( ! is_user_logged_in() ) {
$query['has_password'] = false;
}
if ( isset( $args['include'] ) ) {
$query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] );
}
if ( isset( $args['meta_key'] ) ) {
$show = false;
if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) {
$show = true;
}
if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) {
$show = true;
}
if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) {
return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 );
}
$meta = array( 'key' => $args['meta_key'] );
if ( isset( $args['meta_value'] ) ) {
$meta['value'] = $args['meta_value'];
}
$query['meta_query'] = array( $meta );
}
if ( 'include' === $args['sticky'] ) {
$query['ignore_sticky_posts'] = 1;
} elseif ( 'exclude' === $args['sticky'] ) {
$sticky = get_option( 'sticky_posts' );
if ( is_array( $sticky ) ) {
$query['post__not_in'] = $sticky;
}
} elseif ( 'require' === $args['sticky'] ) {
$sticky = get_option( 'sticky_posts' );
if ( is_array( $sticky ) && ! empty( $sticky ) ) {
$query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky;
} else {
// no sticky posts exist.
return array(
'found' => 0,
'posts' => array(),
);
}
}
if ( isset( $args['exclude'] ) ) {
$excluded_ids = (array) $args['exclude'];
$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids;
}
if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) {
// get_page_children is a misnomer; it supports all hierarchical post types.
$page_args = array(
'child_of' => $args['exclude_tree'],
'post_type' => $args['type'],
// since we're looking for things to exclude, be aggressive.
'post_status' => 'publish,draft,pending,private,future,trash',
);
$post_descendants = get_pages( $page_args );
$exclude_tree = array( $args['exclude_tree'] );
foreach ( $post_descendants as $child ) {
$exclude_tree[] = $child->ID;
}
$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree;
}
if ( isset( $args['category'] ) ) {
$category = get_term_by( 'slug', $args['category'], 'category' );
if ( false === $category ) {
$query['category_name'] = $args['category'];
} else {
$query['cat'] = $category->term_id;
}
}
if ( isset( $args['tag'] ) ) {
$query['tag'] = $args['tag'];
}
if ( ! empty( $args['term'] ) ) {
$query['tax_query'] = array();
foreach ( $args['term'] as $taxonomy => $slug ) {
$taxonomy_object = get_taxonomy( $taxonomy );
if ( false === $taxonomy_object || ( ! $taxonomy_object->public &&
! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) {
continue;
}
$query['tax_query'][] = array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => explode( ',', $slug ),
);
}
}
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$query['paged'] = $args['page'];
if ( 1 !== $query['paged'] ) {
$is_eligible_for_page_handle = false;
}
} else {
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$query['offset'] = $args['offset'];
if ( 0 !== $query['offset'] ) {
$is_eligible_for_page_handle = false;
}
}
if ( isset( $args['before_gmt'] ) ) {
$this->date_range['before'] = $args['before_gmt'];
}
if ( isset( $args['after_gmt'] ) ) {
$this->date_range['after'] = $args['after_gmt'];
}
if ( isset( $args['modified_before_gmt'] ) ) {
$this->modified_range['before'] = $args['modified_before_gmt'];
}
if ( isset( $args['modified_after_gmt'] ) ) {
$this->modified_range['after'] = $args['modified_after_gmt'];
}
if ( $this->date_range ) {
add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
}
if ( $this->modified_range ) {
add_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
}
if ( isset( $args['page_handle'] ) ) {
$page_handle = wp_parse_args( $args['page_handle'] );
if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) {
// we have a valid looking page handle.
$this->page_handle = $page_handle;
add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
}
/**
* 'column' necessary for the me/posts endpoint (which extends sites/$site/posts).
* Would need to be added to the sites/$site/posts definition if we ever want to
* use it there.
*/
$column_whitelist = array( 'post_modified_gmt' );
if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) {
$query['column'] = $args['column'];
}
$this->performed_query = $query;
add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
$wp_query = new WP_Query( $query );
remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
if ( $this->date_range ) {
remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
$this->date_range = array();
}
if ( $this->modified_range ) {
remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
$this->modified_range = array();
}
if ( $this->page_handle ) {
remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
$return = array();
$excluded_count = 0;
foreach ( array_keys( $this->response_format ) as $key ) {
switch ( $key ) {
case 'found':
$return[ $key ] = (int) $wp_query->found_posts;
break;
case 'posts':
$posts = array();
foreach ( $wp_query->posts as $post_ID ) {
$the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] );
if ( $the_post && ! is_wp_error( $the_post ) ) {
$posts[] = $the_post;
} else {
++$excluded_count;
}
}
if ( $posts ) {
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) );
}
$return[ $key ] = $posts;
break;
case 'meta':
if ( ! is_array( $args['type'] ) ) {
$return[ $key ] = (object) array(
'links' => (object) array(
'counts' => (string) $this->links->get_site_link( $blog_id, 'post-counts/' . $args['type'] ),
),
);
}
if ( $is_eligible_for_page_handle && $return['posts'] ) {
$last_post = end( $return['posts'] );
reset( $return['posts'] );
$post_count = is_countable( $return['posts'] ) ? count( $return['posts'] ) : 0;
if ( ( $return['found'] > $post_count ) && $last_post ) {
if ( ! isset( $return[ $key ] ) ) {
$return[ $key ] = (object) array();
}
$handle = $this->build_page_handle( $last_post, $query );
if ( $handle !== null ) {
$return[ $key ]->next_page = $handle;
}
}
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! isset( $return[ $key ] ) ) {
$return[ $key ] = new stdClass();
}
$return[ $key ]->wpcom = true;
}
break;
}
}
$return['found'] -= $excluded_count;
return $return;
}
/**
* Build the page handle.
*
* @param array $post - the post.
* @param array $query - the query.
*/
public function build_page_handle( $post, $query ) {
$column = $query['orderby'];
if ( ! $column ) {
$column = 'date';
}
if ( ! isset( $post['ID'] ) || ! isset( $post[ $column ] ) ) {
return null;
}
return build_query(
array(
'value' => rawurlencode( $post[ $column ] ),
'id' => $post['ID'],
)
);
}
/**
* Build the date range query.
*
* @param string $column - the database column.
* @param array $range - the date range.
* @param string $where - sql where clause.
*/
public function build_date_range_query( $column, $range, $where ) {
global $wpdb;
switch ( count( $range ) ) {
case 2:
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['after'],
$range['before']
);
break;
case 1:
if ( isset( $range['before'] ) ) {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['before']
);
} else {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['after']
);
}
break;
}
return $where;
}
/**
* Handle date range.
*
* @param string $where - sql where clause.
*/
public function handle_date_range( $where ) {
return $this->build_date_range_query( 'post_date_gmt', $this->date_range, $where );
}
/**
* Handle modified date range.
*
* @param string $where - sql where clause.
*/
public function handle_modified_range( $where ) {
return $this->build_date_range_query( 'post_modified_gmt', $this->modified_range, $where );
}
/**
* Handle where clause for page handle.
*
* @param string $where - sql where clause.
*/
public function handle_where_for_page_handle( $where ) {
global $wpdb;
$column = $this->performed_query['orderby'];
if ( ! $column ) {
$column = 'date';
}
$order = $this->performed_query['order'];
if ( ! $order ) {
$order = 'DESC';
}
if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ), true ) ) {
return $where;
}
if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) {
return $where;
}
$db_column = '';
$db_value = '';
switch ( $column ) {
case 'ID':
$db_column = 'ID';
$db_value = '%d';
break;
case 'title':
$db_column = 'post_title';
$db_value = '%s';
break;
case 'date':
$db_column = 'post_date';
$db_value = 'CAST( %s as DATETIME )';
break;
case 'modified':
$db_column = 'post_modified';
$db_value = 'CAST( %s as DATETIME )';
break;
case 'comment_count':
$db_column = 'comment_count';
$db_value = '%d';
break;
}
if ( 'DESC' === $order ) {
$db_order = '<';
} else {
$db_order = '>';
}
// Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item
// but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items
// beyond the passed item.
$where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
if ( 'ID' !== $db_column ) {
$where .= $wpdb->prepare( "OR ( `$wpdb->posts`.`$db_column` = $db_value AND `$wpdb->posts`.ID $db_order %d )", $this->page_handle['value'], $this->page_handle['id'] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
}
$where .= ' )';
return $where;
}
/**
* Handle how the page handle is ordered.
*
* @param string $orderby - what we're ordering by.
*/
public function handle_orderby_for_page_handle( $orderby ) {
global $wpdb;
if ( 'ID' === $this->performed_query['orderby'] ) {
// bail if we're already ordering by ID.
return $orderby;
}
if ( $orderby ) {
$orderby .= ' ,';
}
$order = $this->performed_query['order'];
if ( ! $order ) {
$order = 'DESC';
}
$orderby .= " `$wpdb->posts`.ID $order";
return $orderby;
}
}
@@ -0,0 +1,496 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List posts v1_2 endpoint.
*/
new WPCOM_JSON_API_List_Posts_v1_2_Endpoint(
array(
'description' => 'Get a list of matching posts.',
'min_version' => '1.2',
'max_version' => '1.2',
'group' => 'posts',
'stat' => 'posts',
'method' => 'GET',
'path' => '/sites/%s/posts/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'allow_fallback_to_jetpack_blog_token' => true,
'query_parameters' => array(
'number' => '(int=20) The number of posts to return. Limit: 100.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
'page_handle' => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.',
'order' => array(
'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.',
'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.',
),
'order_by' => array(
'date' => 'Order by the created time of each post.',
'modified' => 'Order by the modified time of each post.',
'title' => "Order lexicographically by the posts' titles.",
'comment_count' => 'Order by the number of comments for each post.',
'ID' => 'Order by post ID.',
),
'after' => '(ISO 8601 datetime) Return posts dated after the specified datetime.',
'before' => '(ISO 8601 datetime) Return posts dated before the specified datetime.',
'modified_after' => '(ISO 8601 datetime) Return posts modified after the specified datetime.',
'modified_before' => '(ISO 8601 datetime) Return posts modified before the specified datetime.',
'tag' => '(string) Specify the tag name or slug.',
'category' => '(string) Specify the category name or slug.',
'term' => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.',
'type' => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'exclude_private_types' => '(bool=false) Use this flag together with `type=any` to get only publicly accessible posts.',
'parent_id' => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.',
'include' => '(array:int|int) Includes the specified post ID(s) in the response',
'exclude' => '(array:int|int) Excludes the specified post ID(s) from the response',
'exclude_tree' => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.',
'status' => '(string) Comma-separated list of statuses for which to query, including any of: "publish", "private", "draft", "pending", "future", and "trash", or simply "any". Defaults to "publish"',
'sticky' => array(
'include' => 'Sticky posts are not excluded from the list.',
'exclude' => 'Sticky posts are excluded from the list.',
'require' => 'Only include sticky posts',
),
'author' => "(int) Author's user ID",
'search' => '(string) Search query',
'meta_key' => '(string) Metadata key that the post should contain',
'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/posts/?number=2',
)
);
/**
* List posts v1_2 endpoint.
*
* /sites/%s/posts/ -> $blog_id
*/
class WPCOM_JSON_API_List_Posts_v1_2_Endpoint extends WPCOM_JSON_API_List_Posts_v1_1_Endpoint { // phpcs:ignore
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$is_eligible_for_page_handle = true;
$site = $this->get_platform()->get_site( $blog_id );
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 100 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
}
if ( isset( $args['type'] ) ) {
// load all types on WPCOM, unless only built-in ones are requested.
if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! in_array( $args['type'], array( 'post', 'revision', 'page' ), true ) ) {
$this->load_theme_functions();
}
if ( ! $site->is_post_type_allowed( $args['type'] ) ) {
return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
}
// Normalize post_type.
if ( 'any' === $args['type'] ) {
$whitelisted_post_types = $site->get_whitelisted_post_types();
if ( ! empty( $args['exclude_private_types'] ) ) {
$public_post_types = get_post_types( array( 'public' => true ) );
$args['type'] = array_intersect( $public_post_types, $whitelisted_post_types );
} else {
$args['type'] = $whitelisted_post_types;
}
}
} else {
// let's be explicit about defaulting to 'post'.
$args['type'] = 'post';
}
// make sure the user can read or edit the requested post type(s).
if ( is_array( $args['type'] ) ) {
$allowed_types = array();
foreach ( $args['type'] as $post_type ) {
if ( $site->current_user_can_access_post_type( $post_type, $args['context'] ) ) {
$allowed_types[] = $post_type;
}
}
if ( empty( $allowed_types ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
$args['type'] = $allowed_types;
} elseif ( ! $site->current_user_can_access_post_type( $args['type'], $args['context'] ) ) {
return array(
'found' => 0,
'posts' => array(),
);
}
// determine statuses.
$status = ( ! empty( $args['status'] ) ) ? explode( ',', $args['status'] ) : array( 'publish' );
if ( is_user_logged_in() ) {
$statuses_whitelist = array(
'publish',
'pending',
'draft',
'future',
'private',
'trash',
'any',
);
$status = array_intersect( $status, $statuses_whitelist );
} else {
// logged-out users can see only published posts.
$statuses_whitelist = array( 'publish', 'any' );
$status = array_intersect( $status, $statuses_whitelist );
if ( empty( $status ) ) {
// requested only protected statuses? nothing for you here.
return array(
'found' => 0,
'posts' => array(),
);
}
// clear it (AKA published only) because "any" includes protected.
$status = array();
}
$query = array(
'posts_per_page' => $args['number'],
'order' => $args['order'],
'orderby' => $args['order_by'],
'post_type' => $args['type'],
'post_status' => $status,
'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null,
'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,
's' => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null,
'fields' => 'ids',
);
if ( ! is_user_logged_in() ) {
$query['has_password'] = false;
}
if ( isset( $args['include'] ) ) {
$query['post__in'] = is_array( $args['include'] ) ? $args['include'] : array( (int) $args['include'] );
}
if ( isset( $args['meta_key'] ) ) {
$show = false;
if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) {
$show = true;
}
if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) {
$show = true;
}
if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) {
return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 );
}
$meta = array( 'key' => $args['meta_key'] );
if ( isset( $args['meta_value'] ) ) {
$meta['value'] = $args['meta_value'];
}
$query['meta_query'] = array( $meta );
}
if ( 'include' === $args['sticky'] ) {
$query['ignore_sticky_posts'] = 1;
} elseif ( 'exclude' === $args['sticky'] ) {
$sticky = get_option( 'sticky_posts' );
if ( is_array( $sticky ) ) {
$query['post__not_in'] = $sticky;
}
} elseif ( 'require' === $args['sticky'] ) {
$sticky = get_option( 'sticky_posts' );
if ( is_array( $sticky ) && ! empty( $sticky ) ) {
$query['post__in'] = isset( $args['include'] ) ? array_merge( $query['post__in'], $sticky ) : $sticky;
} else {
// no sticky posts exist.
return array(
'found' => 0,
'posts' => array(),
);
}
}
if ( isset( $args['exclude'] ) ) {
$excluded_ids = (array) $args['exclude'];
$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids;
}
if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) {
// get_page_children is a misnomer; it supports all hierarchical post types.
$page_args = array(
'child_of' => $args['exclude_tree'],
'post_type' => $args['type'],
// since we're looking for things to exclude, be aggressive.
'post_status' => 'publish,draft,pending,private,future,trash',
);
$post_descendants = get_pages( $page_args );
$exclude_tree = array( $args['exclude_tree'] );
foreach ( $post_descendants as $child ) {
$exclude_tree[] = $child->ID;
}
$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree;
}
if ( isset( $args['category'] ) ) {
$category = get_term_by( 'slug', $args['category'], 'category' );
if ( false === $category ) {
$query['category_name'] = $args['category'];
} else {
$query['cat'] = $category->term_id;
}
}
if ( isset( $args['tag'] ) ) {
$query['tag'] = $args['tag'];
}
if ( ! empty( $args['term'] ) ) {
$query['tax_query'] = array();
foreach ( $args['term'] as $taxonomy => $slug ) {
$taxonomy_object = get_taxonomy( $taxonomy );
if ( false === $taxonomy_object || ( ! $taxonomy_object->public &&
! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) {
continue;
}
$query['tax_query'][] = array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => explode( ',', $slug ),
);
}
}
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$query['paged'] = $args['page'];
if ( 1 !== $query['paged'] ) {
$is_eligible_for_page_handle = false;
}
} else {
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$query['offset'] = $args['offset'];
if ( 0 !== $query['offset'] ) {
$is_eligible_for_page_handle = false;
}
}
if ( isset( $args['before'] ) ) {
$this->date_range['before'] = $args['before'];
}
if ( isset( $args['after'] ) ) {
$this->date_range['after'] = $args['after'];
}
if ( isset( $args['modified_before_gmt'] ) ) {
$this->modified_range['before'] = $args['modified_before_gmt'];
}
if ( isset( $args['modified_after_gmt'] ) ) {
$this->modified_range['after'] = $args['modified_after_gmt'];
}
if ( $this->date_range ) {
add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
}
if ( $this->modified_range ) {
add_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
}
if ( isset( $args['page_handle'] ) ) {
$page_handle = wp_parse_args( $args['page_handle'] );
if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) {
// we have a valid looking page handle.
$this->page_handle = $page_handle;
add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
}
/**
* 'column' necessary for the me/posts endpoint (which extends sites/$site/posts).
* Would need to be added to the sites/$site/posts definition if we ever want to
* use it there.
*/
$column_whitelist = array( 'post_modified_gmt' );
if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist, true ) ) {
$query['column'] = $args['column'];
}
$this->performed_query = $query;
add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
$wp_query = new WP_Query( $query );
remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
if ( $this->date_range ) {
remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
$this->date_range = array();
}
if ( $this->modified_range ) {
remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
$this->modified_range = array();
}
if ( $this->page_handle ) {
remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
}
$return = array();
$excluded_count = 0;
foreach ( array_keys( $this->response_format ) as $key ) {
switch ( $key ) {
case 'found':
$return[ $key ] = (int) $wp_query->found_posts;
break;
case 'posts':
$posts = array();
foreach ( $wp_query->posts as $post_ID ) {
$the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] );
if ( $the_post && ! is_wp_error( $the_post ) ) {
$posts[] = $the_post;
} else {
++$excluded_count;
}
}
if ( $posts ) {
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) );
}
$return[ $key ] = $posts;
break;
case 'meta':
if ( ! is_array( $args['type'] ) ) {
$return[ $key ] = (object) array(
'links' => (object) array(
'counts' => (string) $this->links->get_site_link( $blog_id, 'post-counts/' . $args['type'] ),
),
);
}
if ( $is_eligible_for_page_handle && $return['posts'] ) {
$last_post = end( $return['posts'] );
reset( $return['posts'] );
$post_count = is_countable( $return['posts'] ) ? count( $return['posts'] ) : 0;
if ( ( $return['found'] > $post_count ) && $last_post ) {
if ( ! isset( $return[ $key ] ) ) {
$return[ $key ] = (object) array();
}
$handle = $this->build_page_handle( $last_post, $query );
if ( $handle !== null ) {
$return[ $key ]->next_page = $handle;
}
}
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! isset( $return[ $key ] ) ) {
$return[ $key ] = new stdClass();
}
$return[ $key ]->wpcom = true;
}
break;
}
}
$return['found'] -= $excluded_count;
return $return;
}
/**
* Build page handle.
*
* @param array $post - the post.
* @param array $query - the query.
*/
public function build_page_handle( $post, $query ) {
$column = $query['orderby'];
if ( ! $column ) {
$column = 'date';
}
if ( ! isset( $post['ID'] ) || ! isset( $post[ $column ] ) ) {
return null;
}
return build_query(
array(
'value' => rawurlencode( $post[ $column ] ),
'id' => $post['ID'],
)
);
}
/**
* Build the date range query.
*
* @param string $column - the database column.
* @param array $range - the date range.
* @param string $where - sql where clause.
*/
public function build_date_range_query( $column, $range, $where ) {
global $wpdb;
switch ( count( $range ) ) {
case 2:
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['after'],
$range['before']
);
break;
case 1:
if ( isset( $range['before'] ) ) {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['before']
);
} else {
$where .= $wpdb->prepare(
" AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$range['after']
);
}
break;
}
return $where;
}
}
@@ -0,0 +1,163 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List roles endpoint.
*/
new WPCOM_JSON_API_List_Roles_Endpoint(
array(
'description' => 'List the user roles of a site.',
'group' => '__do_not_document',
'stat' => 'roles:list',
'max_version' => '1.1',
'method' => 'GET',
'path' => '/sites/%s/roles',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(),
'response_format' => array(
'roles' => '(array:role) Array of role objects.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/roles',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
new WPCOM_JSON_API_List_Roles_Endpoint(
array(
'description' => 'List the user roles of a site.',
'group' => '__do_not_document',
'stat' => 'roles:list',
'min_version' => '1.2',
'force' => 'wpcom',
'method' => 'GET',
'path' => '/sites/%s/roles',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(),
'response_format' => array(
'roles' => '(array:role) Array of role objects.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/roles',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List Roles endpoint class.
*
* /sites/%s/roles/ -> $blog_id
*/
class WPCOM_JSON_API_List_Roles_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Response format.
*
* @var array
*/
public $response_format = array(
'roles' => '(array:role) Array of role objects',
);
/**
* Sort so roles with the most number of capabilities comes first, then the next role, and so on.
*
* @param object $a - the first object we're comparing.
* @param object $b - the second object we're comparing.
*/
public static function role_sort( $a, $b ) {
$core_role_names = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
$a_is_core_role = in_array( $a->name, $core_role_names, true );
$b_is_core_role = in_array( $b->name, $core_role_names, true );
// Core roles always come before non-core roles.
if ( $a_is_core_role !== $b_is_core_role ) {
return $b_is_core_role <=> $a_is_core_role;
}
// otherwise the one with the > number of capabilities comes first.
$a_cap_count = is_countable( $a->capabilities ) ? count( $a->capabilities ) : 0;
$b_cap_count = is_countable( $b->capabilities ) ? count( $b->capabilities ) : 0;
return $b_cap_count <=> $a_cap_count;
}
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$roles = array();
$sal_site = $this->get_platform()->get_site( $blog_id );
$wp_roles = $sal_site->get_roles();
// Check if the site is connected and talks to us on a regular basis.
$is_connected = $sal_site->is_connected_site();
if ( is_wp_error( $is_connected ) ) {
return $is_connected;
}
if ( ! $sal_site->current_user_can( 'list_users' ) ) {
return new WP_Error( 'unauthorized', 'User cannot view roles for specified site', 403 );
}
if ( $wp_roles instanceof WP_Roles ) {
$role_names = $wp_roles->get_names();
$role_keys = array_keys( $role_names );
foreach ( (array) $role_keys as $role_key ) {
$role_details = get_role( $role_key );
$role_details->display_name = translate_user_role( $role_names[ $role_key ] );
$roles[] = $role_details;
}
} elseif ( is_array( $wp_roles ) ) {
// Jetpack Shadow Site side of things.
foreach ( $wp_roles as $role_key => $role ) {
$roles[] = (object) array(
'name' => $role_key,
'display_name' => $role['name'],
'capabilities' => (object) $role['capabilities'],
);
}
}
usort( $roles, array( 'self', 'role_sort' ) );
/**
* Filter for curating the list of roles available for a wpcom site.
*
* @module json-api
*
* @since 8.7.0
*
* @param array $roles List of role objects available to the site.
*/
$roles = apply_filters( 'wpcom_api_site_roles', $roles );
return array( 'roles' => $roles );
}
}
@@ -0,0 +1,64 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List shortcodes endpoint.
*/
new WPCOM_JSON_API_List_Shortcodes_Endpoint(
array(
'description' => 'Get a list of shortcodes available on a site. Note: The current user must have publishing access.',
'group' => 'sites',
'stat' => 'shortcodes',
'method' => 'GET',
'path' => '/sites/%s/shortcodes',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'response_format' => array(
'shortcodes' => '(array) A list of supported shortcodes by their handle.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcodes',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List shortcodes endpoint class
*
* /sites/%s/shortcodes -> $blog_id
*/
class WPCOM_JSON_API_List_Shortcodes_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// permissions check.
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 );
}
// list em.
global $shortcode_tags;
$output = array( 'shortcodes' => array() );
foreach ( $shortcode_tags as $tag => $class ) {
if ( '__return_false' === $class ) {
continue;
}
$output['shortcodes'][] = $tag;
}
return $output;
}
}
@@ -0,0 +1,151 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List terms endpoint.
*/
new WPCOM_JSON_API_List_Terms_Endpoint(
array(
'description' => 'Get a list of a site\'s terms by taxonomy.',
'group' => 'taxonomy',
'stat' => 'terms',
'method' => 'GET',
'path' => '/sites/%s/taxonomies/%s/terms',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$taxonomy' => '(string) Taxonomy',
),
'query_parameters' => array(
'number' => '(int=100) The number of terms to return. Limit: 1000.',
'offset' => '(int=0) 0-indexed offset.',
'page' => '(int) Return the Nth 1-indexed page of terms. Takes precedence over the <code>offset</code> parameter.',
'search' => '(string) Limit response to include only terms whose names or slugs match the provided search query.',
'order' => array(
'ASC' => 'Return terms in ascending order.',
'DESC' => 'Return terms in descending order.',
),
'order_by' => array(
'name' => 'Order by the name of each tag.',
'count' => 'Order by the number of posts in each tag.',
),
),
'allow_fallback_to_jetpack_blog_token' => true,
'response_format' => array(
'found' => '(int) The number of terms returned.',
'terms' => '(array) Array of tag objects.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/taxonomies/post_tags/terms?number=5',
)
);
/**
* List terms endpoint class.
*
* /sites/%s/taxonomies/%s/terms -> $blog_id, $taxonomy
*/
class WPCOM_JSON_API_List_Terms_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
* @param string $taxonomy - the taxonomy.
*/
public function callback( $path = '', $blog_id = 0, $taxonomy = 'category' ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
$taxonomy_meta = get_taxonomy( $taxonomy );
if ( false === $taxonomy_meta || ( ! $taxonomy_meta->public &&
! current_user_can( $taxonomy_meta->cap->assign_terms ) ) ) {
return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 );
}
$args = $this->query_args();
$args = $this->process_args( $args );
$formatted_terms = $this->get_formatted_terms( $taxonomy, $args );
if ( ! empty( $formatted_terms ) ) {
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'terms', is_countable( $formatted_terms ) ? count( $formatted_terms ) : 0 );
}
return array(
'found' => (int) $this->get_found( $taxonomy, $args ),
'terms' => (array) $formatted_terms,
);
}
/**
* Process args.
*
* @param array $args - the arguments.
*/
public function process_args( $args ) {
$args['get'] = 'all';
if ( $args['number'] < 1 ) {
$args['number'] = 100;
} elseif ( 1000 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The number parameter must be less than or equal to 1000.', 400 );
}
if ( isset( $args['page'] ) ) {
if ( $args['page'] < 1 ) {
$args['page'] = 1;
}
$args['offset'] = ( $args['page'] - 1 ) * $args['number'];
unset( $args['page'] );
}
if ( $args['offset'] < 0 ) {
$args['offset'] = 0;
}
$args['orderby'] = $args['order_by'];
unset( $args['order_by'] );
unset( $args['context'], $args['pretty'], $args['http_envelope'], $args['fields'] );
return $args;
}
/**
* Get found taxonomy term count.
*
* @param string $taxonomy - the taxonomy.
* @param array $args - the arguments.
*/
public function get_found( $taxonomy, $args ) {
unset( $args['offset'] );
$args['taxonomy'] = $taxonomy;
return wp_count_terms( $args );
}
/**
* Format the taxonomy terms.
*
* @param string $taxonomy - the taxonomy.
* @param array $args - the arguments.
*/
public function get_formatted_terms( $taxonomy, $args ) {
$args['taxonomy'] = $taxonomy;
$terms = get_terms( $args );
$formatted_terms = array();
foreach ( $terms as $term ) {
$formatted_terms[] = $this->format_taxonomy( $term, $taxonomy, 'display' );
}
return $formatted_terms;
}
}
@@ -0,0 +1,257 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* List users endpoint.
*/
new WPCOM_JSON_API_List_Users_Endpoint(
array(
'description' => 'List the users of a site.',
'group' => 'users',
'stat' => 'users:list',
'method' => 'GET',
'path' => '/sites/%s/users',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'number' => '(int=20) Limit the total number of authors returned.',
'offset' => '(int=0) The first n authors to be skipped in the returned array.',
'order' => array(
'DESC' => 'Return authors in descending order.',
'ASC' => 'Return authors in ascending order.',
),
'order_by' => array(
'ID' => 'Order by ID (default).',
'login' => 'Order by username.',
'nicename' => 'Order by nicename.',
'email' => 'Order by author email address.',
'url' => 'Order by author URL.',
'registered' => 'Order by registered date.',
'display_name' => 'Order by display name.',
'post_count' => 'Order by number of posts published.',
),
'authors_only' => '(bool) Set to true to fetch authors only',
'include_viewers' => '(bool) Set to true to include viewers for Simple sites. When you pass in this parameter, order, order_by and search_columns are ignored. Currently, `search` is limited to the first page of results.',
'type' => "(string) Specify the post type to query authors for. Only works when combined with the `authors_only` flag. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'search' => '(string) Find matching users.',
'search_columns' => "(array) Specify which columns to check for matching users. Can be any of 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', and 'display_name'. Only works when combined with `search` parameter.",
'role' => '(string) Specify a specific user role to fetch.',
'capability' => '(string) Specify a specific capability to fetch. You can specify multiple by comma separating them, in which case the user needs to match all capabilities provided.',
),
'response_format' => array(
'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).',
'authors' => '(array:author) Array of author objects.',
),
'example_response' => '{
"found": 1,
"users": [
{
"ID": 78972699,
"login": "apiexamples",
"email": "justin+apiexamples@a8c.com",
"name": "apiexamples",
"first_name": "",
"last_name": "",
"nice_name": "apiexamples",
"URL": "http://apiexamples.wordpress.com",
"avatar_URL": "https://1.gravatar.com/avatar/a2afb7b6c0e23e5d363d8612fb1bd5ad?s=96&d=identicon&r=G",
"profile_URL": "https://gravatar.com/apiexamples",
"site_ID": 82974409,
"roles": [
"administrator"
],
"is_super_admin": false
}
]
}',
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/users',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* List users endpoint class.
*
* /sites/%s/users/ -> $blog_id
*/
class WPCOM_JSON_API_List_Users_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* The response format.
*
* @var array
*/
public $response_format = array(
'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).',
'users' => '(array:author) Array of user objects',
);
/**
* API callback.
*
* @param string $path - the path.
* @param string $blog_id - the blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$args = $this->query_args();
$authors_only = ( ! empty( $args['authors_only'] ) );
if ( $args['number'] < 1 ) {
$args['number'] = 20;
} elseif ( 1000 < $args['number'] ) {
return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 1000.', 400 );
}
if ( $authors_only ) {
if ( empty( $args['type'] ) ) {
$args['type'] = 'post';
}
if ( ! $this->is_post_type_allowed( $args['type'] ) ) {
return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
}
$post_type_object = get_post_type_object( $args['type'] );
if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
return new WP_Error( 'unauthorized', 'User cannot view authors for specified post type', 403 );
}
} elseif ( ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'unauthorized', 'User cannot view users for specified site', 403 );
}
$query = array(
'number' => $args['number'],
'offset' => $args['offset'],
'order' => $args['order'],
'orderby' => $args['order_by'],
'fields' => 'ID',
);
if ( $authors_only ) {
$query['capability'] = array( 'edit_posts' );
}
if ( ! empty( $args['search'] ) ) {
$query['search'] = $args['search'];
}
if ( ! empty( $args['search_columns'] ) ) {
// this `user_search_columns` filter is necessary because WP_User_Query does not allow `display_name` as a search column.
$this->search_columns = array_intersect( $args['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', 'display_name' ) );
add_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ), 10, 3 );
}
if ( ! empty( $args['role'] ) ) {
$query['role'] = $args['role'];
}
if ( ! empty( $args['capability'] ) ) {
$query['capability'] = $args['capability'];
}
$user_query = new WP_User_Query( $query );
remove_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ) );
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
$include_viewers = (bool) isset( $args['include_viewers'] ) && $args['include_viewers'] && $is_wpcom;
$page = ( (int) ( $args['offset'] / $args['number'] ) ) + 1;
$viewers = $include_viewers ? get_private_blog_users(
$blog_id,
array(
'page' => $page,
'per_page' => $args['number'],
)
) : array();
$viewers = array_map( array( $this, 'get_author' ), $viewers );
// we restrict search field to name when include_viewers is true.
if ( $include_viewers && ! empty( $args['search'] ) ) {
$viewers = array_filter(
$viewers,
function ( $viewer ) use ( $args ) {
// remove special database search characters from search term
$search_term = str_replace( '*', '', $args['search'] );
return strpos( $viewer->name, $search_term ) !== false;
}
);
}
$return = array();
foreach ( array_keys( $this->response_format ) as $key ) {
switch ( $key ) {
case 'found':
$user_count = (int) $user_query->get_total();
$viewer_count = 0;
if ( $include_viewers ) {
if ( empty( $args['search'] ) ) {
$viewer_count = (int) get_count_private_blog_users( $blog_id );
} else {
$viewer_count = count( $viewers );
}
}
$return[ $key ] = $user_count + $viewer_count;
break;
case 'users':
$users = array();
$is_multisite = is_multisite();
foreach ( $user_query->get_results() as $u ) {
$the_user = $this->get_author( $u, true );
if ( $the_user && ! is_wp_error( $the_user ) ) {
$userdata = get_userdata( $u );
$the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array();
if ( $is_multisite ) {
$the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' );
}
$users[] = $the_user;
}
}
$combined_users = array_merge( $users, $viewers );
// When viewers are included, we ignore the order & orderby parameters.
if ( $include_viewers ) {
usort(
$combined_users,
function ( $a, $b ) {
return strcmp( strtolower( $a->name ), strtolower( $b->name ) );
}
);
}
$return[ $key ] = $combined_users;
break;
}
}
return $return;
}
/**
* Override search columns.
*
* @param array $search_columns - the search column we're overriding.
* @param array $search - the search query.
*/
public function api_user_override_search_columns( $search_columns, $search ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return $this->search_columns;
}
}
@@ -0,0 +1,647 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Post Endpoint class.
*/
abstract class WPCOM_JSON_API_Post_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Post object format.
*
* @var array
*/
public $post_object_format = array(
// explicitly document and cast all output
'ID' => '(int) The post ID.',
'site_ID' => '(int) The site ID.',
'author' => '(object>author) The author of the post.',
'date' => "(ISO 8601 datetime) The post's creation time.",
'modified' => "(ISO 8601 datetime) The post's most recent update time.",
'title' => '(HTML) <code>context</code> dependent.',
'URL' => '(URL) The full permalink URL to the post.',
'short_URL' => '(URL) The wp.me short URL.',
'content' => '(HTML) <code>context</code> dependent.',
'excerpt' => '(HTML) <code>context</code> dependent.',
'slug' => '(string) The name (slug) for the post, used in URLs.',
'guid' => '(string) The GUID for the post.',
'status' => array(
'publish' => 'The post is published.',
'draft' => 'The post is saved as a draft.',
'pending' => 'The post is pending editorial approval.',
'private' => 'The post is published privately',
'future' => 'The post is scheduled for future publishing.',
'trash' => 'The post is in the trash.',
'auto-draft' => 'The post is a placeholder for a new post.',
),
'sticky' => '(bool) Is the post sticky?',
'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
'parent' => "(object>post_reference|false) A reference to the post's parent, if it has one.",
'type' => "(string) The post's post_type. Post types besides post, page and revision need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'comments_open' => '(bool) Is the post open for comments?',
'pings_open' => '(bool) Is the post open for pingbacks, trackbacks?',
'likes_enabled' => '(bool) Is the post open to likes?',
'sharing_enabled' => '(bool) Should sharing buttons show on this post?',
'comment_count' => '(int) The number of comments for this post.',
'like_count' => '(int) The number of likes for this post.',
'i_like' => '(bool) Does the current user like this post?',
'is_reblogged' => '(bool) Did the current user reblog this post?',
'is_following' => '(bool) Is the current user following this blog?',
'global_ID' => '(string) A unique WordPress.com-wide representation of a post.',
'featured_image' => '(URL) The URL to the featured image for this post if it has one.',
'post_thumbnail' => '(object>attachment) The attachment object for the featured image if it has one.',
'format' => array(), // see constructor
'geo' => '(object>geo|false)',
'menu_order' => '(int) (Pages Only) The order pages should appear in.',
'publicize_URLs' => '(array:URL) Array of Facebook URLs published by this post.',
'tags' => '(object:tag) Hash of tags (keyed by tag name) applied to the post.',
'categories' => '(object:category) Hash of categories (keyed by category name) applied to the post.',
'attachments' => '(object:attachment) Hash of post attachments (keyed by attachment ID).',
'metadata' => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.',
'meta' => '(object) API result meta data',
'current_user_can' => '(object) List of permissions. Note, deprecated in favor of `capabilities`',
'capabilities' => '(object) List of post-specific permissions for the user; publish_post, edit_post, delete_post',
);
/**
* Constructor function.
*
* @param string|array|object $args — Args.
*/
public function __construct( $args ) {
if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) {
$this->post_object_format['format'] = get_post_format_strings();
}
if ( ! $this->response_format ) {
$this->response_format =& $this->post_object_format;
}
parent::__construct( $args );
}
/**
* Filter to replace the password form with a simple message that the post is protected.
*
* @return string
*/
public function the_password_form() {
return __( 'This post is password protected.', 'jetpack' );
}
/**
* Get a post by a specified field and value
*
* @param string $field - the field.
* @param string $field_value - the field value.
* @param string $context Post use context (e.g. 'display').
* @return array|bool|WP_Error Post
**/
public function get_post_by( $field, $field_value, $context = 'display' ) {
global $blog_id;
/** This filter is documented in class.json-api-endpoints.php */
$is_jetpack = true === apply_filters( 'is_jetpack_site', false, $blog_id );
if ( defined( 'GEO_LOCATION__CLASS' ) && class_exists( GEO_LOCATION__CLASS ) ) {
$geo = call_user_func( array( GEO_LOCATION__CLASS, 'init' ) );
} else {
$geo = false;
}
if ( 'display' === $context ) {
$args = $this->query_args();
if ( isset( $args['content_width'] ) && $args['content_width'] ) {
$GLOBALS['content_width'] = (int) $args['content_width'];
}
}
switch ( $field ) {
case 'name':
$post_id = $this->get_post_id_by_name( $field_value );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
break;
default:
$post_id = (int) $field_value;
break;
}
$post = get_post( $post_id, OBJECT, $context );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
if ( ! $this->is_post_type_allowed( $post->post_type ) && ( ! function_exists( 'is_post_freshly_pressed' ) || ! is_post_freshly_pressed( $post->ID ) ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
// Permissions
$capabilities = $this->get_current_user_capabilities( $post );
switch ( $context ) {
case 'edit':
if ( ! $capabilities['edit_post'] ) {
return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
}
break;
case 'display':
break;
default:
return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
}
$can_view = $this->user_can_view_post( $post->ID );
if ( ! $can_view || is_wp_error( $can_view ) ) {
return $can_view;
}
$GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
if ( 'display' === $context ) {
setup_postdata( $post );
}
$response = array();
$fields = null;
if ( 'display' === $context && ! empty( $this->api->query['fields'] ) ) {
$fields = array_fill_keys( array_map( 'trim', explode( ',', $this->api->query['fields'] ) ), true );
}
foreach ( array_keys( $this->post_object_format ) as $key ) {
if ( $fields !== null && ! isset( $fields[ $key ] ) ) {
continue;
}
switch ( $key ) {
case 'ID':
// explicitly cast all output
$response[ $key ] = (int) $post->ID;
break;
case 'site_ID':
$response[ $key ] = (int) $this->api->get_blog_id_for_output();
break;
case 'author':
$response[ $key ] = (object) $this->get_author( $post, 'edit' === $context && $capabilities['edit_post'] );
break;
case 'date':
$response[ $key ] = (string) $this->format_date( $post->post_date_gmt, $post->post_date );
break;
case 'modified':
$response[ $key ] = (string) $this->format_date( $post->post_modified_gmt, $post->post_modified );
break;
case 'title':
if ( 'display' === $context ) {
$response[ $key ] = (string) get_the_title( $post->ID );
} else {
$response[ $key ] = (string) htmlspecialchars_decode( $post->post_title, ENT_QUOTES );
}
break;
case 'URL':
if ( 'revision' === $post->post_type ) {
$response[ $key ] = (string) esc_url_raw( get_permalink( $post->post_parent ) );
} else {
$response[ $key ] = (string) esc_url_raw( get_permalink( $post->ID ) );
}
break;
case 'short_URL':
$response[ $key ] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) );
break;
case 'content':
if ( 'display' === $context ) {
add_filter( 'the_password_form', array( $this, 'the_password_form' ) );
$response[ $key ] = (string) $this->get_the_post_content_for_display();
remove_filter( 'the_password_form', array( $this, 'the_password_form' ) );
} else {
$response[ $key ] = (string) $post->post_content;
}
break;
case 'excerpt':
if ( 'display' === $context ) {
add_filter( 'the_password_form', array( $this, 'the_password_form' ) );
ob_start();
the_excerpt();
$response[ $key ] = (string) ob_get_clean();
remove_filter( 'the_password_form', array( $this, 'the_password_form' ) );
} else {
$response[ $key ] = htmlspecialchars_decode( (string) $post->post_excerpt, ENT_QUOTES );
}
break;
case 'status':
$response[ $key ] = (string) get_post_status( $post->ID );
break;
case 'sticky':
$response[ $key ] = (bool) is_sticky( $post->ID );
break;
case 'slug':
$response[ $key ] = (string) $post->post_name;
break;
case 'guid':
$response[ $key ] = (string) $post->guid;
break;
case 'password':
$response[ $key ] = (string) $post->post_password;
if ( 'edit' === $context ) {
$response[ $key ] = htmlspecialchars_decode( (string) $response[ $key ], ENT_QUOTES );
}
break;
/** (object|false) */
case 'parent':
if ( $post->post_parent ) {
$parent = get_post( $post->post_parent );
if ( 'display' === $context ) {
$parent_title = (string) get_the_title( $parent->ID );
} else {
$parent_title = (string) htmlspecialchars_decode( $post->post_title, ENT_QUOTES );
}
$response[ $key ] = (object) array(
'ID' => (int) $parent->ID,
'type' => (string) $parent->post_type,
'link' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $parent->ID ),
'title' => $parent_title,
);
} else {
$response[ $key ] = false;
}
break;
case 'type':
$response[ $key ] = (string) $post->post_type;
break;
case 'comments_open':
$response[ $key ] = (bool) comments_open( $post->ID );
break;
case 'pings_open':
$response[ $key ] = (bool) pings_open( $post->ID );
break;
case 'likes_enabled':
/** This filter is documented in modules/likes.php */
$sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
$post_likes_switched = get_post_meta( $post->ID, 'switch_like_status', true );
$post_likes_enabled = $post_likes_switched || ( $sitewide_likes_enabled && $post_likes_switched !== '0' );
$response[ $key ] = (bool) $post_likes_enabled;
break;
case 'sharing_enabled':
$show = true;
/** This filter is documented in modules/sharedaddy/sharing-service.php */
$show = apply_filters( 'sharing_show', $show, $post );
$switched_status = get_post_meta( $post->ID, 'sharing_disabled', false );
if ( ! empty( $switched_status ) ) {
$show = false;
}
$response[ $key ] = (bool) $show;
break;
case 'comment_count':
$response[ $key ] = (int) $post->comment_count;
break;
case 'like_count':
$response[ $key ] = (int) $this->api->post_like_count( $blog_id, $post->ID );
break;
case 'i_like':
$response[ $key ] = (bool) $this->api->is_liked( $blog_id, $post->ID );
break;
case 'is_reblogged':
$response[ $key ] = (bool) $this->api->is_reblogged( $blog_id, $post->ID );
break;
case 'is_following':
$response[ $key ] = (bool) $this->api->is_following( $blog_id );
break;
case 'global_ID':
$response[ $key ] = (string) $this->api->add_global_ID( $blog_id, $post->ID );
break;
case 'featured_image':
if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
$response[ $key ] = get_post_meta( $post->ID, '_jetpack_featured_image', true );
} else {
$image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
$response[ $key ] = (string) $image_attributes[0];
} else {
$response[ $key ] = '';
}
}
break;
case 'post_thumbnail':
$response[ $key ] = null;
$thumb_id = get_post_thumbnail_id( $post->ID );
if ( ! empty( $thumb_id ) ) {
$attachment = get_post( $thumb_id );
if ( ! empty( $attachment ) ) {
$featured_image_object = $this->get_attachment( $attachment );
}
if ( ! empty( $featured_image_object ) ) {
$response[ $key ] = (object) $featured_image_object;
}
}
break;
case 'format':
$response[ $key ] = (string) get_post_format( $post->ID );
if ( ! $response[ $key ] ) {
$response[ $key ] = 'standard';
}
break;
/** (object|false) */
case 'geo':
if ( ! $geo ) {
$response[ $key ] = false;
} else {
$geo_data = $geo->get_geo( 'post', $post->ID );
$response[ $key ] = false;
if ( $geo_data ) {
$geo_data = array_intersect_key(
$geo_data,
array(
'latitude' => true,
'longitude' => true,
'address' => true,
'public' => true,
)
);
if ( $geo_data ) {
$response[ $key ] = (object) array(
'latitude' => isset( $geo_data['latitude'] ) ? (float) $geo_data['latitude'] : 0,
'longitude' => isset( $geo_data['longitude'] ) ? (float) $geo_data['longitude'] : 0,
'address' => isset( $geo_data['address'] ) ? (string) $geo_data['address'] : '',
);
} else {
$response[ $key ] = false;
}
// Private
if ( ! isset( $geo_data['public'] ) || ! $geo_data['public'] ) {
if ( 'edit' !== $context || ! $capabilities['edit_post'] ) {
// user can't access
$response[ $key ] = false;
}
}
}
}
break;
case 'menu_order':
$response[ $key ] = (int) $post->menu_order;
break;
case 'publicize_URLs':
$publicize_urls = array();
$publicize = get_post_meta( $post->ID, 'publicize_results', true );
if ( $publicize ) {
foreach ( $publicize as $service => $data ) {
switch ( $service ) {
// @todo Explore removing once Twitter has been removed from Publicize.
case 'twitter':
foreach ( $data as $datum ) {
$publicize_urls[] = esc_url_raw( "https://twitter.com/{$datum['user_id']}/status/{$datum['post_id']}" );
}
break;
case 'fb':
foreach ( $data as $datum ) {
$publicize_urls[] = esc_url_raw( "https://www.facebook.com/permalink.php?story_fbid={$datum['post_id']}&id={$datum['user_id']}" );
}
break;
}
}
}
$response[ $key ] = (array) $publicize_urls;
break;
case 'tags':
$response[ $key ] = array();
$terms = wp_get_post_tags( $post->ID );
foreach ( $terms as $term ) {
if ( ! empty( $term->name ) ) {
$response[ $key ][ $term->name ] = $this->format_taxonomy( $term, 'post_tag', 'display' );
}
}
$response[ $key ] = (object) $response[ $key ];
break;
case 'categories':
$response[ $key ] = array();
$terms = wp_get_object_terms( $post->ID, 'category', array( 'fields' => 'all' ) );
foreach ( $terms as $term ) {
if ( ! empty( $term->name ) ) {
$response[ $key ][ $term->name ] = $this->format_taxonomy( $term, 'category', 'display' );
}
}
$response[ $key ] = (object) $response[ $key ];
break;
case 'attachments':
$response[ $key ] = array();
$_attachments = get_posts(
array(
'post_parent' => $post->ID,
'post_status' => 'inherit',
'post_type' => 'attachment',
'posts_per_page' => 100,
)
);
foreach ( $_attachments as $attachment ) {
$response[ $key ][ $attachment->ID ] = $this->get_attachment( $attachment );
}
$response[ $key ] = (object) $response[ $key ];
break;
/** (array|false) */
case 'metadata':
$metadata = array();
foreach ( (array) has_meta( $post_id ) as $meta ) {
// Don't expose protected fields.
$show = false;
if ( WPCOM_JSON_API_Metadata::is_public( $meta['meta_key'] ) ) {
$show = true;
}
if ( current_user_can( 'edit_post_meta', $post_id, $meta['meta_key'] ) ) {
$show = true;
}
if (
in_array( $meta['meta_key'], Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) &&
! Jetpack_SEO_Utils::is_enabled_jetpack_seo()
) {
$show = false;
}
if ( ! $show ) {
continue;
}
$metadata[] = array(
'id' => $meta['meta_id'],
'key' => $meta['meta_key'],
'value' => maybe_unserialize( $meta['meta_value'] ),
);
}
if ( ! empty( $metadata ) ) {
$response[ $key ] = $metadata;
} else {
$response[ $key ] = false;
}
break;
case 'meta':
$response[ $key ] = (object) array(
'links' => (object) array(
'self' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ),
'help' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'help' ),
'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
'replies' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'replies/' ),
'likes' => (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'likes/' ),
),
);
break;
case 'current_user_can':
$response[ $key ] = $capabilities;
break;
case 'capabilities':
$response[ $key ] = $capabilities;
break;
}
}
unset( $GLOBALS['post'] );
return $response;
}
/**
*
* Get the post content for display.
*
* No Blog ID parameter. No Post ID parameter. Depends on globals.
* Expects setup_postdata() to already have been run.
*
* @return string|false
*/
public function get_the_post_content_for_display() {
global $pages, $page;
$old_pages = $pages;
$old_page = $page;
$content = implode( "\n\n", $pages );
$content = preg_replace( '/<!--more(.*?)?-->/', '', $content );
$pages = array( $content ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$page = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
ob_start();
the_content();
$return = ob_get_clean();
$pages = $old_pages; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$page = $old_page; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
return $return;
}
/**
* Gets the blog post.
*
* @param int $blog_id - the blog ID.
* @param int $post_id - the post ID.
* @param string $context - the context.
* @return array|bool|WP_Error Post
*/
public function get_blog_post( $blog_id, $post_id, $context = 'display' ) {
$blog_id = $this->api->get_blog_id( $blog_id );
if ( ! $blog_id || is_wp_error( $blog_id ) ) {
return $blog_id;
}
switch_to_blog( $blog_id );
$post = $this->get_post_by( 'ID', $post_id, $context );
restore_current_blog();
return $post;
}
/**
* Supporting featured media in post endpoints. Currently on for wpcom blogs
* since it's calling WPCOM_JSON_API_Read_Endpoint methods which presently
* rely on wpcom specific functionality.
*
* @param WP_Post $post - the WP Post object.
* @return object list of featured media
*/
public static function find_featured_media( &$post ) {
if ( class_exists( 'WPCOM_JSON_API_Read_Endpoint' ) ) {
return WPCOM_JSON_API_Read_Endpoint::find_featured_worthy_media( (array) $post );
} else {
return (object) array();
}
}
/**
* Returns attachment object.
*
* @param object $attachment attachment row.
*
* @return object
*/
public function get_attachment( $attachment ) {
$metadata = wp_get_attachment_metadata( $attachment->ID );
$result = array(
'ID' => (int) $attachment->ID,
'URL' => (string) wp_get_attachment_url( $attachment->ID ),
'guid' => (string) $attachment->guid,
'mime_type' => (string) $attachment->post_mime_type,
'width' => (int) isset( $metadata['width'] ) ? $metadata['width'] : 0,
'height' => (int) isset( $metadata['height'] ) ? $metadata['height'] : 0,
);
if ( isset( $metadata['duration'] ) ) {
$result['duration'] = (int) $metadata['duration'];
}
return (object) apply_filters( 'get_attachment', $result );
}
/**
* Get post-specific user capabilities
*
* @param WP_Post $post - the WP_Post object.
*
* @return array - array of post-level permissions; 'publish_post', 'delete_post', 'edit_post'
*/
public function get_current_user_capabilities( $post ) {
return array(
'publish_post' => current_user_can( 'publish_post', $post->ID ),
'delete_post' => current_user_can( 'delete_post', $post->ID ),
'edit_post' => current_user_can( 'edit_post', $post->ID ),
);
}
/**
* Get post ID by name
*
* Attempts to match name on post title and page path
*
* @param string $name - the name of the post.
*
* @return int|object Post ID on success, WP_Error object on failure
**/
protected function get_post_id_by_name( $name ) {
$name = sanitize_title( $name );
if ( ! $name ) {
return new WP_Error( 'invalid_post', 'Invalid post', 400 );
}
$posts = get_posts(
array(
'name' => $name,
'numberposts' => 1,
'post_type' => $this->_get_whitelisted_post_types(),
)
);
if ( ! $posts || ! isset( $posts[0]->ID ) || ! $posts[0]->ID ) {
$page = get_page_by_path( $name );
if ( ! $page ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
$post_id = $page->ID;
} else {
$post_id = (int) $posts[0]->ID;
}
return $post_id;
}
}
@@ -0,0 +1,378 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Post v1_1 Endpoint class.
*/
abstract class WPCOM_JSON_API_Post_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore PEAR.NamingConventions.ValidClassName.Invalid, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
/**
* Post object format.
*
* @var array
*/
public $post_object_format = array(
// explicitly document and cast all output
'ID' => '(int) The post ID.',
'site_ID' => '(int) The site ID.',
'author' => '(object>author) The author of the post.',
'date' => "(ISO 8601 datetime) The post's creation time.",
'modified' => "(ISO 8601 datetime) The post's most recent update time.",
'title' => '(HTML) <code>context</code> dependent.',
'URL' => '(URL) The full permalink URL to the post.',
'short_URL' => '(URL) The wp.me short URL.',
'content' => '(HTML) <code>context</code> dependent.',
'excerpt' => '(HTML) <code>context</code> dependent.',
'slug' => '(string) The name (slug) for the post, used in URLs.',
'guid' => '(string) The GUID for the post.',
'status' => array(
'publish' => 'The post is published.',
'draft' => 'The post is saved as a draft.',
'pending' => 'The post is pending editorial approval.',
'private' => 'The post is published privately',
'future' => 'The post is scheduled for future publishing.',
'trash' => 'The post is in the trash.',
'auto-draft' => 'The post is a placeholder for a new post.',
),
'sticky' => '(bool) Is the post sticky?',
'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
'parent' => "(object>post_reference|false) A reference to the post's parent, if it has one.",
'type' => "(string) The post's post_type. Post types besides post, page and revision need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
'discussion' => '(object) Hash of discussion options for the post',
'likes_enabled' => '(bool) Is the post open to likes?',
'sharing_enabled' => '(bool) Should sharing buttons show on this post?',
'like_count' => '(int) The number of likes for this post.',
'i_like' => '(bool) Does the current user like this post?',
'is_reblogged' => '(bool) Did the current user reblog this post?',
'is_following' => '(bool) Is the current user following this blog?',
'global_ID' => '(string) A unique WordPress.com-wide representation of a post.',
'featured_image' => '(URL) The URL to the featured image for this post if it has one.',
'post_thumbnail' => '(object>attachment) The attachment object for the featured image if it has one.',
'format' => array(), // see constructor
'geo' => '(object>geo|false)',
'menu_order' => '(int) (Pages Only) The order pages should appear in.',
'page_template' => '(string) (Pages Only) The page template this page is using.',
'publicize_URLs' => '(array:URL) Array of Facebook URLs published by this post.',
'terms' => '(object) Hash of taxonomy names mapping to a hash of terms keyed by term name.',
'tags' => '(object:tag) Hash of tags (keyed by tag name) applied to the post.',
'categories' => '(object:category) Hash of categories (keyed by category name) applied to the post.',
'attachments' => '(object:attachment) Hash of post attachments (keyed by attachment ID). Returns the most recent 20 attachments. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.',
'attachment_count' => '(int) The total number of attachments for this post. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.',
'metadata' => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.',
'meta' => '(object) API result meta data',
'capabilities' => '(object) List of post-specific permissions for the user; publish_post, edit_post, delete_post',
'revisions' => '(array) List of post revision IDs. Only available for posts retrieved with context=edit.',
'other_URLs' => '(object) List of URLs for this post. Permalink and slug suggestions.',
);
/**
* Constructor function.
*
* @param string|array|object $args — Args.
*/
public function __construct( $args ) {
if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) {
$this->post_object_format['format'] = get_post_format_strings();
}
if ( ! $this->response_format ) {
$this->response_format =& $this->post_object_format;
}
parent::__construct( $args );
}
/**
* Get a post by a specified field and value
*
* @param string $field - the field.
* @param string $field_value - the field value.
* @param string $context Post use context (e.g. 'display').
*
* @return array|WP_Error Post
**/
public function get_post_by( $field, $field_value, $context = 'display' ) {
// validate input
if ( ! in_array( $field, array( 'ID', 'name' ), true ) ) {
return new WP_Error( 'invalid_field', 'Invalid API FIELD', 400 );
}
if ( ! in_array( $context, array( 'display', 'edit' ), true ) ) {
return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
}
if ( 'display' === $context ) {
$args = $this->query_args();
if ( isset( $args['content_width'] ) && $args['content_width'] ) {
$GLOBALS['content_width'] = (int) $args['content_width'];
}
}
// fetch SAL post
$post = $this->get_sal_post_by( $field, $field_value, $context );
if ( is_wp_error( $post ) ) {
return $post;
}
$GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
// TODO: not sure where this one should go
if ( 'display' === $context ) {
setup_postdata( $post );
}
$keys_to_render = array_keys( $this->post_object_format );
if ( isset( $this->api->query['fields'] ) ) {
$limit_to_fields = array_map( 'trim', explode( ',', $this->api->query['fields'] ) );
$keys_to_render = array_intersect( $keys_to_render, $limit_to_fields );
}
// always include some keys because processors require it to validate access
$keys_to_render = array_unique( array_merge( $keys_to_render, array( 'type', 'status', 'password' ) ) );
$response = $this->render_response_keys( $post, $context, $keys_to_render );
unset( $GLOBALS['post'] );
return $response;
}
/**
* Get SAL post by a specified field and value
*
* @param string $field - the field.
* @param string $field_value - the field value.
* @param string $context Post use context (e.g. 'display').
*
* @return SAL_Post|WP_Error Post
**/
protected function get_sal_post_by( $field, $field_value, $context ) {
global $blog_id;
$site = $this->get_platform()->get_site( $blog_id );
$post = ( $field === 'name' ) ?
$site->get_post_by_name( $field_value, $context ) :
$site->get_post_by_id( $field_value, $context );
return $post;
}
/**
* Render the response keys.
*
* @param object $post - the post.
* @param string $context Post use context (e.g. 'display').
* @param array $keys - the keys.
*
* @return array
*/
private function render_response_keys( $post, $context, $keys ) {
$response = array();
foreach ( $keys as $key ) {
switch ( $key ) {
case 'ID':
// explicitly cast all output
$response[ $key ] = (int) $post->ID;
break;
case 'site_ID':
$response[ $key ] = $post->site->get_id();
break;
case 'author':
$response[ $key ] = $post->get_author();
break;
case 'date':
$response[ $key ] = $post->get_date();
break;
case 'modified':
$response[ $key ] = $post->get_modified_date();
break;
case 'title':
$response[ $key ] = $post->get_title();
break;
case 'URL':
$response[ $key ] = $post->get_url();
break;
case 'short_URL':
$response[ $key ] = $post->get_shortlink();
break;
case 'content':
$response[ $key ] = $post->get_content();
break;
case 'excerpt':
$response[ $key ] = $post->get_excerpt();
break;
case 'status':
$response[ $key ] = $post->get_status();
break;
case 'sticky':
$response[ $key ] = $post->is_sticky();
break;
case 'slug':
$response[ $key ] = $post->get_slug();
break;
case 'guid':
$response[ $key ] = $post->get_guid();
break;
case 'password':
$response[ $key ] = $post->get_password();
break;
/** (object|false) */
case 'parent':
$response[ $key ] = $post->get_parent();
break;
case 'type':
$response[ $key ] = $post->get_type();
break;
case 'discussion':
$response[ $key ] = $post->get_discussion();
break;
case 'likes_enabled':
$response[ $key ] = $post->is_likes_enabled();
break;
case 'sharing_enabled':
$response[ $key ] = $post->is_sharing_enabled();
break;
case 'like_count':
$response[ $key ] = $post->get_like_count();
break;
case 'i_like':
$response[ $key ] = $post->is_liked();
break;
case 'is_reblogged':
$response[ $key ] = $post->is_reblogged();
break;
case 'is_following':
$response[ $key ] = $post->is_following();
break;
case 'global_ID':
$response[ $key ] = $post->get_global_id();
break;
case 'featured_image':
$response[ $key ] = $post->get_featured_image();
break;
case 'post_thumbnail':
$response[ $key ] = $post->get_post_thumbnail();
break;
case 'format':
$response[ $key ] = $post->get_format();
break;
/** (object|false) */
case 'geo':
$response[ $key ] = $post->get_geo();
break;
case 'menu_order':
$response[ $key ] = $post->get_menu_order();
break;
case 'page_template':
$response[ $key ] = $post->get_page_template();
break;
case 'publicize_URLs':
$response[ $key ] = $post->get_publicize_urls();
break;
case 'terms':
$response[ $key ] = $post->get_terms();
break;
case 'tags':
$response[ $key ] = $post->get_tags();
break;
case 'categories':
$response[ $key ] = $post->get_categories();
break;
case 'attachments':
list( $attachments, $attachment_count ) = $post->get_attachments_and_count();
$response[ $key ] = $attachments;
$response['attachment_count'] = $attachment_count;
break;
/** (array|false) */
case 'metadata':
$response[ $key ] = $post->get_metadata();
break;
case 'meta':
$response[ $key ] = $post->get_meta();
break;
case 'capabilities':
$response[ $key ] = $post->get_current_user_capabilities();
break;
case 'revisions':
$revisions = $post->get_revisions();
if ( $revisions ) {
$response[ $key ] = $revisions;
}
break;
case 'other_URLs':
$response[ $key ] = $post->get_other_urls();
break;
}
}
return $response;
}
/**
* Filter respnse.
*
* @param array $response - the response.
* @return array Filtered response.
*/
public function filter_response( $response ) {
// Do minimal processing if the caller didn't request it
if ( ! isset( $_REQUEST['meta_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site.
return $response;
}
// Bail early if we do not have the necessary data.
if (
is_wp_error( $response )
|| ! isset( $response['posts'] )
|| ! is_array( $response['posts'] )
) {
return $response;
}
// Retrieve an array of field paths, such as: [`autosave.modified`, `autosave.post_ID`]
$fields = explode( ',', sanitize_text_field( wp_unslash( $_REQUEST['meta_fields'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site.
foreach ( $response['posts'] as $post ) {
if ( ! isset( $post['meta'] ) || ! isset( $post['meta']->data ) || ( ! is_array( $post['meta']->data ) && ! is_object( $post['meta']->data ) ) ) {
continue;
}
$newmeta = array();
foreach ( $post['meta']->data as $field_key => $field_value ) {
foreach ( $field_value as $subfield_key => $subfield_value ) {
$key_path = $field_key . '.' . $subfield_key;
if ( in_array( $key_path, $fields, true ) ) {
$newmeta[ $field_key ][ $subfield_key ] = $subfield_value;
}
}
}
$post['meta']->data = $newmeta;
}
return $response;
}
/**
* Gets the blog post.
*
* @param int $blog_id - the blog ID.
* @param int $post_id - the post ID.
* @param string $context - the context.
*
* @return array|bool|WP_Error
*/
public function get_blog_post( $blog_id, $post_id, $context = 'display' ) {
$blog_id = $this->api->get_blog_id( $blog_id );
if ( ! $blog_id || is_wp_error( $blog_id ) ) {
return $blog_id;
}
switch_to_blog( $blog_id );
$post = $this->get_post_by( 'ID', $post_id, $context );
restore_current_blog();
return $post;
}
}
@@ -0,0 +1,89 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Render_Embed_Endpoint(
array(
'description' => 'Get a rendered embed for a site. Note: The current user must have publishing access.',
'group' => 'sites',
'stat' => 'embeds:render',
'method' => 'GET',
'path' => '/sites/%s/embeds/render',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'embed_url' => '(string) The query-string encoded embed URL to render. Required. Only accepts one at a time.',
),
'response_format' => array(
'embed_url' => '(string) The embed_url that was passed in for rendering.',
'result' => '(html) The rendered HTML result of the embed.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/apiexamples.wordpress.com/embeds/render?embed_url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSQEQr7c0-dw',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Render embed endpoint class.
*
* /sites/%s/embeds/render -> $blog_id
*/
class WPCOM_JSON_API_Render_Embed_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', __( 'Your token must have permission to post on this blog.', 'jetpack' ), 403 );
}
$args = $this->query_args();
$embed_url = trim( $args['embed_url'] );
// quick validation
if ( ! preg_match_all( '|^\s*(https?://[^\s"]+)\s*$|im', $embed_url, $matches ) ) {
return new WP_Error( 'invalid_embed_url', __( 'The embed_url parameter must be a valid URL.', 'jetpack' ), 400 );
}
if ( is_countable( $matches[1] ) && count( $matches[1] ) > 1 ) {
return new WP_Error( 'invalid_embed', __( 'Only one embed can be rendered at a time.', 'jetpack' ), 400 );
}
$embed_url = array_shift( $matches[1] );
$parts = wp_parse_url( $embed_url );
if ( ! $parts ) {
return new WP_Error( 'invalid_embed_url', __( 'The embed_url parameter must be a valid URL.', 'jetpack' ), 400 );
}
global $wp_embed;
$render = $this->process_render( array( $this, 'do_embed' ), $embed_url );
// if nothing happened, then the shortcode does not exist.
$is_an_embed = ( $embed_url !== $render['result'] && $wp_embed->maybe_make_link( $embed_url ) !== $render['result'] );
if ( ! $is_an_embed ) {
return new WP_Error( 'invalid_embed', __( 'The requested URL is not an embed.', 'jetpack' ), 400 );
}
// our output for this endpoint..
$return = array();
$return['embed_url'] = $embed_url;
$return['result'] = $render['result'];
$return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] );
return $return;
}
}
@@ -0,0 +1,138 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Render_Embed_Reversal_Endpoint(
array(
'description' => 'Determines if the given embed code can be reversed into a single line embed or a shortcode, and if so returns the embed or shortcode. Note: The current user must have publishing access.',
'group' => '__do_not_document',
'stat' => 'embeds:reversal',
'method' => 'POST',
'path' => '/sites/%s/embeds/reversal',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'maybe_embed' => '(string) The embed code to reverse. Required. Only accepts one at a time.',
),
'response_format' => array(
'maybe_embed' => '(string) The original embed code that was passed in for rendering.',
'reversal_type' => '(string) The type of reversal. Either an embed or a shortcode.',
'render_result' => '(html) The rendered HTML result of the embed or shortcode.',
'result' => '(string) The reversed content. Either a single line embed or a shortcode.',
'scripts' => '(array) An array of JavaScript files needed to render the embed or shortcode. Returned in the format of <code>{ "script-slug" : { "src": "http://example.com/file.js", "extra" : "" } }</code> where extra contains any neccessary extra JS for initializing the source file and src contains the script to load. Omitted if no scripts are neccessary.',
'styles' => '(array) An array of CSS files needed to render the embed or shortcode. Returned in the format of <code>{ "style-slug" : { "src": "http://example.com/file.css", "media" : "all" } }</code>. Omitted if no styles are neccessary.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcode-reversals/render/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'maybe_embed' => '<iframe width="480" height="302" src="http://www.ustream.tv/embed/recorded/26370522/highlight/299667?v=3&amp;wmode=direct" scrolling="no" frameborder="0"></iframe>',
),
),
)
);
/**
* Render embed reversal class.
*
* /sites/%s/embeds/reversal -> $blog_id
*/
class WPCOM_JSON_API_Render_Embed_Reversal_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
/**
* API callback.
*
* @param string $path - the path (unused).
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 );
}
$is_embed = false;
$is_shortcode = false;
$input = $this->input( true );
$maybe_embed = trim( $input['maybe_embed'] );
if ( empty( $maybe_embed ) ) {
return new WP_Error( 'empty_embed', 'Please provide an embed code to process.', 400 );
}
$ksesed_content = trim( wp_strip_all_tags( wp_kses_post( $maybe_embed ), true ) );
if ( empty( $ksesed_content ) ) {
return new WP_Error( 'invalid_embed', 'Invalid or empty embed provided.', 400 );
}
$shortcode_pattern = get_shortcode_regex();
$url_pattern = '/^http(s)?:\/\/[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(\/.*)?$/i';
preg_match_all( "/$shortcode_pattern/s", $ksesed_content, $shortcode_matches );
preg_match_all( "$url_pattern", $ksesed_content, $url_matches );
if ( empty( $shortcode_matches[0] ) && empty( $url_matches[0] ) ) {
return new WP_Error( 'invalid_embed', 'The provided embed is not supported.', 400 );
}
$shortcode_matches_count = is_countable( $shortcode_matches[0] ) ? count( $shortcode_matches[0] ) : 0;
$url_matches_count = is_countable( $url_matches[0] ) ? count( $url_matches[0] ) : 0;
if ( ( $shortcode_matches_count + $url_matches_count ) > 1 ) {
return new WP_Error( 'invalid_embed', 'Only one embed/shortcode reversal can be rendered at a time.', 400 );
}
if ( ! empty( $shortcode_matches[0] ) ) {
$is_shortcode = true;
} elseif ( ! empty( $url_matches[0] ) ) {
$is_embed = true;
}
$render = $this->process_render(
array( $this, 'render_shortcode_reversal' ),
array(
'shortcode_reversal' => $ksesed_content,
'is_shortcode' => $is_shortcode,
'is_embed' => $is_embed,
)
);
// if nothing happened, then the shortcode does not exist.
global $wp_embed;
if ( empty( $render ) || empty( $render['result'] ) || $ksesed_content === $render['result'] || $wp_embed->maybe_make_link( $maybe_embed ) === $render['result'] ) {
return new WP_Error( 'invalid_embed', 'The provided embed is not supported.', 400 );
}
// our output for this endpoint..
$return = array();
$return['maybe_embed'] = $maybe_embed;
$return['result'] = $ksesed_content;
$return['reversal_type'] = ( $is_embed ) ? 'embed' : 'shortcode';
$return['render_result'] = $render['result'];
$return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] );
return $return;
}
/**
* Render the shortcode reversal.
*
* @param array $args - the arguments.
*
* @return mixed|false
*/
public function render_shortcode_reversal( $args ) {
if ( $args['is_shortcode'] ) {
return $this->do_shortcode( $args['shortcode_reversal'] );
} elseif ( $args['is_embed'] ) {
return $this->do_embed( $args['shortcode_reversal'] );
}
return false;
}
}
@@ -0,0 +1,167 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* These are helpers for the shortcode and embed render endpoints.
*/
abstract class WPCOM_JSON_API_Render_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Figure out what scripts and styles to load.
* props to o2's o2_Read_API::poll() function for inspiration.
*
* In short we figure out what scripts load for a "normal" page load by executing wp_head and wp_footer
* then we render the embed/shortcode (to both get our result, and to have the shortcode files enqueue their resources)
* then we load wp_head and wp_footer again to see what new resources were added
* finally we find out the url to the source file and any extra info (like media or init js)
*
* @param mixed $callback - the function callback.
* @param mixed $callback_arg - the callback arguments.
*
* @return array
*/
public function process_render( $callback, $callback_arg ) {
global $wp_scripts, $wp_styles;
if ( false === defined( 'STYLESHEETPATH' ) ) {
wp_templating_constants();
}
// initial scripts & styles (to subtract)
ob_start();
wp_head();
wp_footer();
ob_end_clean();
$initial_scripts = $wp_scripts->done;
$initial_styles = $wp_styles->done;
// actually render the shortcode, get the result, and do the resource loading again so we can subtract..
ob_start();
wp_head();
ob_end_clean();
$result = call_user_func( $callback, $callback_arg );
ob_start();
wp_footer();
ob_end_clean();
// find the difference (the new resource files)
$loaded_scripts = array_diff( $wp_scripts->done, $initial_scripts );
$loaded_styles = array_diff( $wp_styles->done, $initial_styles );
return array(
'result' => $result,
'loaded_scripts' => $loaded_scripts,
'loaded_styles' => $loaded_styles,
);
}
/**
* Takes the list of styles and scripts and adds them to the JSON response.
*
* @param array $return - what was returned.
* @param array $loaded_scripts - the loaded scripts.
* @param array $loaded_styles - the loaded styles.
*
* @return array
*/
public function add_assets( $return, $loaded_scripts, $loaded_styles ) {
global $wp_scripts, $wp_styles;
// scripts first, just cuz
if ( $loaded_scripts !== array() ) {
$scripts = array();
foreach ( $loaded_scripts as $handle ) {
if ( ! isset( $wp_scripts->registered[ $handle ] ) ) {
continue;
}
$src = $wp_scripts->registered[ $handle ]->src;
// attach version and an extra query parameters
$ver = $this->get_version( $wp_scripts->registered[ $handle ]->ver, $wp_scripts->default_version );
if ( isset( $wp_scripts->args[ $handle ] ) ) {
$ver = $ver ? $ver . '&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 ( $loaded_styles !== array() ) {
$styles = array();
foreach ( $loaded_styles as $handle ) {
if ( ! isset( $wp_styles->registered[ $handle ] ) ) {
continue;
}
$src = $wp_styles->registered[ $handle ]->src;
// attach version and an extra query parameters
$ver = $this->get_version( $wp_styles->registered[ $handle ]->ver, $wp_styles->default_version );
if ( isset( $wp_styles->args[ $handle ] ) ) {
$ver = $ver ? $ver . '&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.
*
* @param string $this_scripts_version - this scripts version.
* @param string $default_version - the default version.
*
* @return string
*/
public function get_version( $this_scripts_version, $default_version ) {
if ( null === $this_scripts_version ) {
$ver = '';
} else {
$ver = $this_scripts_version ? $this_scripts_version : $default_version;
}
return $ver;
}
/**
* Given a shortcode, process and return the result.
*
* @param string $shortcode - the shortcode.
*/
public function do_shortcode( $shortcode ) {
return do_shortcode( $shortcode );
}
/**
* Given a one-line embed URL, process and return the result.
*
* @param string $embed_url - the embed URL.
*
* @return string|false
*/
public function do_embed( $embed_url ) {
global $wp_embed;
return $wp_embed->shortcode( array(), $embed_url );
}
}
@@ -0,0 +1,86 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Render_Shortcode_Endpoint(
array(
'description' => 'Get a rendered shortcode for a site. Note: The current user must have publishing access.',
'group' => 'sites',
'stat' => 'shortcodes:render',
'method' => 'GET',
'path' => '/sites/%s/shortcodes/render',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'shortcode' => '(string) The query-string encoded shortcode string to render. Required. Only accepts one at a time.',
),
'response_format' => array(
'shortcode' => '(string) The shortcode that was passed in for rendering.',
'result' => '(html) The rendered HTML result of the shortcode.',
'scripts' => '(array) An array of JavaScript files needed to render the shortcode. Returned in the format of <code>{ "script-slug" : { "src": "http://example.com/file.js", "extra" : "" } }</code> where extra contains any neccessary extra JS for initializing the source file and src contains the script to load. Omitted if no scripts are neccessary.',
'styles' => '(array) An array of CSS files needed to render the shortcode. Returned in the format of <code>{ "style-slug" : { "src": "http://example.com/file.css", "media" : "all" } }</code>. Omitted if no styles are neccessary.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/shortcodes/render?shortcode=%5Bgallery%20ids%3D%22729%2C732%2C731%2C720%22%5D',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Render shortcode endpoint class.
*
* /sites/%s/shortcodes/render -> $blog_id
*/
class WPCOM_JSON_API_Render_Shortcode_Endpoint extends WPCOM_JSON_API_Render_Endpoint {
/**
* The API callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'unauthorized', 'Your token must have permission to post on this blog.', 403 );
}
$args = $this->query_args();
$shortcode = trim( $args['shortcode'] );
// Quick validation - shortcodes should always be enclosed in brackets []
if ( ! wp_startswith( $shortcode, '[' ) || ! wp_endswith( $shortcode, ']' ) ) {
return new WP_Error( 'invalid_shortcode', 'The shortcode parameter must begin and end with square brackets.', 400 );
}
// Make sure only one shortcode is being rendered at a time
$pattern = get_shortcode_regex();
preg_match_all( "/$pattern/s", $shortcode, $matches );
if ( is_countable( $matches[0] ) && count( $matches[0] ) > 1 ) {
return new WP_Error( 'invalid_shortcode', 'Only one shortcode can be rendered at a time.', 400 );
}
$render = $this->process_render( array( $this, 'do_shortcode' ), $shortcode );
// if nothing happened, then the shortcode does not exist.
if ( $shortcode === $render['result'] ) {
return new WP_Error( 'invalid_shortcode', 'The requested shortcode does not exist.', 400 );
}
// our output for this endpoint..
$return = array();
$return['shortcode'] = $shortcode;
$return['result'] = $render['result'];
$return = $this->add_assets( $return, $render['loaded_scripts'], $render['loaded_styles'] );
return $return;
}
}
@@ -0,0 +1,787 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
/**
* Sharing button endpoint class.
*/
abstract class WPCOM_JSON_API_Sharing_Button_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* All visibilties.
*
* @var array
*/
public static $all_visibilities = array( 'visible', 'hidden' );
/**
* Sharing service.
*
* @var Sharing_Service
*/
protected $sharing_service;
/**
* Setup function.
*
* @return null|WP_Error
*/
protected function setup() {
if ( class_exists( 'Sharing_Service' ) ) {
$this->sharing_service = new Sharing_Service();
}
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'forbidden', 'You do not have the capability to manage sharing buttons for this site', 403 );
} elseif ( ! class_exists( 'Sharing_Service' ) || ! class_exists( 'Sharing_Source' ) ||
( method_exists( 'Jetpack', 'is_module_active' ) && ! Jetpack::is_module_active( 'sharedaddy' ) ) ) {
return new WP_Error( 'missing_jetpack_module', 'The Sharing module must be activated in order to use this endpoint', 400 );
}
}
/**
* Format the sharing button.
*
* @param object $button - the button object.
* @return array
*/
public function format_sharing_button( $button ) {
$response = array(
'ID' => $button->get_id(),
'name' => $button->get_name(),
'shortname' => $button->shortname,
'custom' => is_a( $button, 'Share_Custom' ),
'enabled' => $this->is_button_enabled( $button ),
);
if ( $response['enabled'] ) {
// Status is either "disabled" or the visibility value
$response['visibility'] = $this->get_button_visibility( $button );
}
if ( ! empty( $button->icon ) ) {
// Only pre-defined sharing buttons include genericon
$response['genericon'] = $button->icon;
}
if ( method_exists( $button, 'get_options' ) ) {
// merge get_options() values into response, primarily to account
// for custom sharing button values
foreach ( $button->get_options() as $key => $value ) {
// Capitalize URL property
if ( 'url' === strtolower( $key ) ) {
$key = strtoupper( $key );
}
$response[ $key ] = $value;
}
}
return $response;
}
/**
* Get the button visibility.
*
* @param object $button - the button object.
*
* @return string|false
*/
public function get_button_visibility( $button ) {
$services = $this->sharing_service->get_blog_services();
$visibilities = self::$all_visibilities;
$button_id = $button->get_id();
foreach ( $visibilities as $visibility ) {
if ( isset( $services[ $visibility ][ $button_id ] ) ) {
return $visibility;
}
}
return false;
}
/**
* Check if the button is enabled.
*
* @param object $button - the button object.
*
* @return bool
*/
public function is_button_enabled( $button ) {
return false !== $this->get_button_visibility( $button );
}
/**
* Check if button is for custom (?).
*
* @param array $button - the button array.
*
* @return bool
*/
protected function is_button_input_for_custom( $button ) {
return ( isset( $button['custom'] ) && $button['custom'] ) ||
( isset( $button['ID'] ) && 1 === preg_match( '/^custom-/', $button['ID'] ) ) ||
! empty( $button['name'] ) || ! empty( $button['URL'] ) || ! empty( $button['icon'] );
}
/**
* Validate the button input.
*
* @param array $button - the button array.
* @param bool $is_new - if the button is new.
*
* @return null|WP_Error
*/
protected function validate_button_input( $button, $is_new = false ) {
if ( ! empty( $button['visibility'] ) && ! in_array( $button['visibility'], self::$all_visibilities, true ) ) {
return new WP_Error( 'invalid_visibility', sprintf( 'The visibility field must be one of the following values: %s', implode( ', ', self::$all_visibilities ) ), 400 );
} elseif ( $is_new && empty( $button['URL'] ) ) {
return new WP_Error( 'invalid_request', 'The URL field is required', 400 );
} elseif ( $is_new && empty( $button['icon'] ) ) {
return new WP_Error( 'invalid_request', 'The icon field is required', 400 );
}
}
/**
* Create a custom button.
*
* @param array $button - the button array.
*
* @return Share_Custom|false
*/
public function create_custom_button( $button ) {
// Default visibility to 'visible' if enabled
if ( empty( $button['visibility'] ) && true === $button['enabled'] ) {
$button['visibility'] = 'visible';
}
$updated_service = $this->sharing_service->new_service( $button['name'], $button['URL'], $button['icon'] );
if ( false !== $updated_service && ( true === $button['enabled'] || ! empty( $button['visibility'] ) ) ) {
$blog_services = $this->sharing_service->get_blog_services();
$blog_services[ $button['visibility'] ][ (string) $updated_service->get_id() ] = $updated_service;
$this->sharing_service->set_blog_services( array_keys( $blog_services['visible'] ), array_keys( $blog_services['hidden'] ) );
}
return $updated_service;
}
/**
* Update the button.
*
* @param int $button_id - the button id.
* @param array $button - the button array.
*
* @return Share_Custom|WP_Error
*/
public function update_button( $button_id, $button ) {
$blog_services = $this->sharing_service->get_blog_services();
// Find existing button
$all_buttons = $this->sharing_service->get_all_services_blog();
if ( ! array_key_exists( $button_id, $all_buttons ) ) {
// Button doesn't exist
return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 );
}
$updated_service = $all_buttons[ $button_id ];
$service_id = $updated_service->get_id();
if ( is_a( $all_buttons[ $button_id ], 'Share_Custom' ) ) {
// Replace options for existing custom button
$options = $updated_service->get_options();
$name = isset( $button['name'] ) ? $button['name'] : $options['name'];
$url = isset( $button['URL'] ) ? $button['URL'] : $options['url'];
$icon = isset( $button['icon'] ) ? $button['icon'] : $options['icon'];
$updated_service = new Share_Custom(
$service_id,
array(
'name' => $name,
'url' => $url,
'icon' => $icon,
)
);
$this->sharing_service->set_service( $button_id, $updated_service );
}
// Default visibility to 'visible' if enabled
if ( empty( $button['visibility'] ) && true === $button['enabled'] ) {
$button['visibility'] = 'visible';
} elseif ( false === $button['enabled'] ) {
unset( $button['visibility'] );
}
// Update button visibility and enabled status
$visibility_changed = ( isset( $button['visibility'] ) || true === $button['enabled'] ) && ! array_key_exists( $service_id, $blog_services[ $button['visibility'] ] );
$is_disabling = false === $button['enabled'];
if ( $visibility_changed || $is_disabling ) {
// Remove from all other visibilities
foreach ( $blog_services as $service_visibility => $services ) {
if ( $is_disabling || $service_visibility !== $button['visibility'] ) {
unset( $blog_services[ $service_visibility ][ $service_id ] );
}
}
if ( $visibility_changed ) {
$blog_services[ $button['visibility'] ][ $service_id ] = $updated_service;
}
$this->sharing_service->set_blog_services( array_keys( $blog_services['visible'] ), array_keys( $blog_services['hidden'] ) );
}
return $updated_service;
}
}
new WPCOM_JSON_API_Get_Sharing_Buttons_Endpoint(
array(
'description' => 'Get a list of a site\'s sharing buttons.',
'group' => 'sharing',
'stat' => 'sharing-buttons',
'method' => 'GET',
'path' => '/sites/%s/sharing-buttons/',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'enabled_only' => '(bool) If true, only enabled sharing buttons are included in the response',
'visibility' => '(string) The type of enabled sharing buttons to filter by, either "visible" or "hidden"',
),
'response_format' => array(
'found' => '(int) The total number of sharing buttons found that match the request.',
'sharing_buttons' => '(array:object) Array of sharing button objects',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
),
'example_response' => '
{
"found": 2,
"sharing_buttons": [
{
"ID": "twitter",
"name": "Twitter",
"shortname": "twitter",
"custom": false,
"enabled": true,
"visibility": "visible",
"genericon": "\\f202"
},
{
"ID": "facebook",
"name": "Facebook",
"shortname": "facebook",
"custom": false,
"enabled": true,
"visibility": "visible",
"genericon": "\\f203"
}
]
}',
)
);
/**
* Get sharing buttons endpoint class.
*
* GET /sites/%s/sharing-buttons -> $blog_id
*/
class WPCOM_JSON_API_Get_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
$args = $this->query_args();
// Validate request
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$continue = $this->setup();
if ( is_wp_error( $continue ) ) {
return $continue;
}
if ( ! empty( $args['visibility'] ) && ! in_array( $args['visibility'], self::$all_visibilities, true ) ) {
return new WP_Error( 'invalid_visibility', sprintf( 'The visibility field must be one of the following values: %s', implode( ', ', self::$all_visibilities ) ), 400 );
}
// Determine which visibilities to include based on request
$visibilities = empty( $args['visibility'] ) ? self::$all_visibilities : array( $args['visibility'] );
// Discover enabled services
$buttons = array();
$enabled_services = $this->sharing_service->get_blog_services();
$all_services = $this->sharing_service->get_all_services_blog();
// Include buttons of desired visibility
foreach ( $visibilities as $visibility ) {
$buttons = array_merge( $buttons, $enabled_services[ $visibility ] );
}
// Unless `enabled_only` or `visibility` is specified, append the
// remaining buttons to the end of the array
if ( ( ! isset( $args['enabled_only'] ) || ! $args['enabled_only'] ) && empty( $args['visibility'] ) ) {
foreach ( $all_services as $id => $button ) {
if ( ! array_key_exists( $id, $buttons ) ) {
$buttons[ $id ] = $button;
}
}
}
// Format each button in the response
$response = array();
foreach ( $buttons as $button ) {
$response[] = $this->format_sharing_button( $button );
}
return array(
'found' => count( $response ),
'sharing_buttons' => $response,
);
}
}
new WPCOM_JSON_API_Get_Sharing_Button_Endpoint(
array(
'description' => 'Get information about a single sharing button.',
'group' => '__do_not_document',
'stat' => 'sharing-buttons:1',
'method' => 'GET',
'path' => '/sites/%s/sharing-buttons/%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$button_id' => '(string) The button ID',
),
'response_format' => array(
'ID' => '(int) Sharing button ID',
'name' => '(string) Sharing button name, used as a label on the button itself',
'shortname' => '(string) A generated short name for the sharing button',
'URL' => '(string) The URL pattern defined for a custom sharing button',
'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button',
'genericon' => '(string) Icon character in Genericons icon set',
'custom' => '(bool) Is the button a user-created custom sharing button?',
'enabled' => '(bool) Is the button currently enabled for the site?',
'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/facebook',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
),
'example_response' => '{
"ID": "facebook",
"name": "Facebook",
"shortname": "facebook",
"custom": false,
"enabled": true,
"visibility": "visible",
"genericon": "\\f203"
}',
)
);
/**
* Get sharing button endpoint class.
*
* GET /sites/%s/sharing-buttons/%s -> $blog_id, $button_id
*/
class WPCOM_JSON_API_Get_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $button_id - the button id.
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $button_id = 0 ) {
// Validate request
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$continue = $this->setup();
if ( is_wp_error( $continue ) ) {
return $continue;
}
// Search existing services for button
$all_buttons = $this->sharing_service->get_all_services_blog();
if ( ! array_key_exists( $button_id, $all_buttons ) ) {
return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 );
} else {
return $this->format_sharing_button( $all_buttons[ $button_id ] );
}
}
}
new WPCOM_JSON_API_Update_Sharing_Buttons_Endpoint(
array(
'description' => 'Edit all sharing buttons for a site.',
'group' => 'sharing',
'stat' => 'sharing-buttons:X:POST',
'method' => 'POST',
'path' => '/sites/%s/sharing-buttons',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'sharing_buttons' => '(array:sharing_button) An array of sharing button objects',
),
'response_format' => array(
'success' => '(bool) Confirmation that all sharing buttons were updated as specified',
'updated' => '(array) An array of updated sharing buttons',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'sharing_buttons' => array(
array(
'ID' => 'facebook',
'visibility' => 'hidden',
),
),
),
),
'example_response' => '{
"success": true,
"updated": [
{
"ID": "facebook",
"name": "Facebook",
"shortname": "facebook",
"custom": false,
"enabled": true,
"visibility": "hidden",
"genericon": "\\f204"
}
]
}',
)
);
/**
* Update sharing buttons endpoint.
*
* POST /sites/%s/sharing-buttons -> $blog_id
*/
class WPCOM_JSON_API_Update_Sharing_Buttons_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
$input = $this->input();
// Validate request
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$continue = $this->setup();
if ( is_wp_error( $continue ) ) {
return $continue;
}
$all_buttons = $this->sharing_service->get_all_services_blog();
if ( ! isset( $input['sharing_buttons'] ) ) {
$input['sharing_buttons'] = array();
}
// We do a first pass of all buttons to verify that no validation
// issues exist before continuing to update
foreach ( $input['sharing_buttons'] as $button ) {
$button_exists = isset( $button['ID'] ) && array_key_exists( $button['ID'], $all_buttons );
$is_custom = $this->is_button_input_for_custom( $button );
// If neither custom nor existing, bail
if ( ! $button_exists && ! $is_custom ) {
return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 );
}
// Validate input, only testing custom values if the button doesn't
// already exist
$validation_error = $this->validate_button_input( $button, ! $button_exists );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
}
// Reset all existing buttons
$this->sharing_service->set_blog_services( array(), array() );
// Finally, we iterate over each button and update or create
$success = true;
$updated = array();
foreach ( $input['sharing_buttons'] as $button ) {
$button_exists = isset( $button['ID'] ) && array_key_exists( $button['ID'], $all_buttons );
if ( $button_exists ) {
$updated_service = $this->update_button( $button['ID'], $button );
} else {
$updated_service = $this->create_custom_button( $button );
}
// We'll allow the request to continue if a failure occurred, but
// log it for the response
if ( false === $updated_service ) {
$success = false;
} else {
$updated[] = $this->format_sharing_button( $updated_service );
}
}
return array(
'success' => $success,
'updated' => $updated,
);
}
}
new WPCOM_JSON_API_Update_Sharing_Button_Endpoint(
array(
'description' => 'Create a new custom sharing button.',
'group' => '__do_not_document',
'stat' => 'sharing-buttons:new',
'method' => 'POST',
'path' => '/sites/%s/sharing-buttons/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'name' => '(string) The name for your custom sharing button, used as a label on the button itself',
'URL' => '(string) The URL to use for share links, including optional placeholders (%post_id%, %post_title%, %post_slug%, %post_url%, %post_full_url%, %post_excerpt%, %post_tags%, %home_url%)',
'icon' => '(string) The full URL to a 16x16 icon to display on the sharing button',
'enabled' => '(bool) Is the button currently enabled for the site?',
'visibility' => '(string) If enabled, the visibility of the sharing button, either "visible" (default) or "hidden"',
),
'response_format' => array(
'ID' => '(string) Sharing button ID',
'name' => '(string) Sharing button name, used as a label on the button itself',
'shortname' => '(string) A generated short name for the sharing button',
'URL' => '(string) The URL pattern defined for a custom sharing button',
'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button',
'genericon' => '(string) Icon character in Genericons icon set',
'custom' => '(bool) Is the button a user-created custom sharing button?',
'enabled' => '(bool) Is the button currently enabled for the site?',
'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/new/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'name' => 'Custom',
'URL' => 'https://www.wordpress.com/%post_name%',
'icon' => 'https://en.wordpress.com/i/stats-icon.gif',
'enabled' => true,
'visibility' => 'visible',
),
),
'example_response' => '{
"ID": "custom-123456789",
"name": "Custom",
"shortname": "custom",
"url": "https://www.wordpress.com/%post_name%",
"icon": "https://en.wordpress.com/i/stats-icon.gif",
"custom": true,
"enabled": true,
"visibility": "visible"
}',
)
);
new WPCOM_JSON_API_Update_Sharing_Button_Endpoint(
array(
'description' => 'Edit a sharing button.',
'group' => '__do_not_document',
'stat' => 'sharing-buttons:1:POST',
'method' => 'POST',
'path' => '/sites/%s/sharing-buttons/%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$button_id' => '(string) The button ID',
),
'request_format' => array(
'name' => '(string) Only if a custom sharing button, a new name used as a label on the button itself',
'URL' => '(string) Only if a custom sharing button, the URL to use for share links, including optional placeholders (%post_title%, %post_url%, %post_full_url%, %post_excerpt%, %post_tags%)',
'icon' => '(string) Only if a custom sharing button, the full URL to a 16x16 icon to display on the sharing button',
'enabled' => '(bool) Is the button currently enabled for the site?',
'visibility' => '(string) If enabled, the visibility of the sharing button, either "visible" (default) or "hidden"',
),
'response_format' => array(
'ID' => '(string) Sharing button ID',
'name' => '(string) Sharing button name, used as a label on the button itself',
'shortname' => '(string) A generated short name for the sharing button',
'URL' => '(string) The URL pattern defined for a custom sharing button',
'icon' => '(string) URL to the 16x16 icon defined for a custom sharing button',
'genericon' => '(string) Icon character in Genericons icon set',
'custom' => '(bool) Is the button a user-created custom sharing button?',
'enabled' => '(bool) Is the button currently enabled for the site?',
'visibility' => '(string) If enabled, the current visibility of the sharing button, either "visible" or "hidden"',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/custom-123456789/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'enabled' => false,
),
),
'example_response' => '{
"ID": "custom-123456789",
"name": "Custom",
"shortname": "custom",
"custom": true,
"enabled": false,
"icon": "https://en.wordpress.com/i/stats-icon.gif",
"url": "https://www.wordpress.com/%post_name%"
}',
)
);
/**
* Sharing button endpoint class.
*
* POST /sites/%s/sharing-buttons/new -> $blog_id
* POST /sites/%s/sharing-buttons/%s -> $blog_id, $button_id
*/
class WPCOM_JSON_API_Update_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $button_id - the button ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $button_id = 0 ) {
$new = $this->api->ends_with( $path, '/new' );
$input = $this->input();
// Validate request
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$continue = $this->setup();
if ( is_wp_error( $continue ) ) {
return $continue;
}
$validation_error = $this->validate_button_input( $input, $new );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
// Update or create button
if ( $new ) {
$updated_service = $this->create_custom_button( $input );
} else {
$updated_service = $this->update_button( $button_id, $input );
}
if ( false === $updated_service ) {
return new WP_Error( 'invalid_request', sprintf( 'The sharing button was not %s', $new ? 'created' : 'updated' ), 400 );
} elseif ( is_wp_error( $updated_service ) ) {
return $updated_service;
} else {
return $this->format_sharing_button( $updated_service );
}
}
}
new WPCOM_JSON_API_Delete_Sharing_Button_Endpoint(
array(
'description' => 'Delete a custom sharing button.',
'group' => '__do_not_document',
'stat' => 'sharing-buttons:1:delete',
'method' => 'POST',
'path' => '/sites/%s/sharing-buttons/%s/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$button_id' => '(string) The button ID',
),
'response_format' => array(
'ID' => '(int) The ID of the deleted sharing button',
'success' => '(bool) Confirmation that the sharing button has been removed',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/sharing-buttons/custom-123456789/delete',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
),
'example_response' => '{
"ID": "custom-123456789",
"success": "true"
}',
)
);
/**
* Delete sharing button endpoint class.
*
* POST /sites/%s/sharing-buttons/%s/delete -> $blog_id, $button_id
*/
class WPCOM_JSON_API_Delete_Sharing_Button_Endpoint extends WPCOM_JSON_API_Sharing_Button_Endpoint {
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $button_id - the button ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $button_id = 0 ) {
// Validate request
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$continue = $this->setup();
if ( is_wp_error( $continue ) ) {
return $continue;
}
// Find existing button
$all_buttons = $this->sharing_service->get_all_services_blog();
if ( ! array_key_exists( $button_id, $all_buttons ) ) {
// Button doesn't exist
return new WP_Error( 'not_found', 'The specified sharing button was not found', 404 );
}
// Verify button is custom
if ( ! is_a( $all_buttons[ $button_id ], 'Share_Custom' ) ) {
return new WP_Error( 'invalid_request', 'Only custom sharing buttons can be deleted', 400 );
}
$success = $this->sharing_service->delete_service( $button_id );
return array(
'ID' => $button_id,
'success' => $success,
);
}
}
@@ -0,0 +1,214 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Site_Settings_V1_2_Endpoint(
array(
'description' => 'Get detailed settings information about a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.2',
'method' => 'GET',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'context' => false,
),
'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
new WPCOM_JSON_API_Site_Settings_V1_2_Endpoint(
array(
'description' => 'Update settings for a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'blogname' => '(string) Blog name',
'blogdescription' => '(string) Blog description',
'default_pingback_flag' => '(bool) Notify blogs linked from article?',
'default_ping_status' => '(bool) Allow link notifications from other blogs?',
'default_comment_status' => '(bool) Allow comments on new articles?',
'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines',
'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati',
'jetpack_relatedposts_enabled' => '(bool) Enable related posts?',
'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?',
'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?',
'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?',
'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface',
'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
'jetpack_search_supported' => '(bool) Jetpack Search supported',
'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow',
'infinite_scroll' => '(bool) Support infinite scroll of posts?',
'default_category' => '(int) Default post category',
'default_post_format' => '(string) Default post format',
'require_name_email' => '(bool) Require comment authors to fill out name and email?',
'comment_registration' => '(bool) Require users to be registered and logged in to comment?',
'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?',
'close_comments_days_old' => '(int) Age at which to close comments',
'thread_comments' => '(bool) Enable threaded comments?',
'thread_comments_depth' => '(int) Depth to thread comments',
'page_comments' => '(bool) Break comments into pages?',
'comments_per_page' => '(int) Number of comments to display per page',
'default_comments_page' => '(string) newest|oldest Which page of comments to display first',
'comment_order' => '(string) asc|desc Order to display comments within page',
'comments_notify' => '(bool) Email me when someone comments?',
'moderation_notify' => '(bool) Email me when a comment is helf for moderation?',
'social_notifications_like' => '(bool) Email me when someone likes my post?',
'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?',
'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?',
'comment_moderation' => '(bool) Moderate comments for manual approval?',
'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?',
'comment_max_links' => '(int) Moderate comments that contain X or more links',
'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line',
'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line',
'lang_id' => '(int) ID for language blog is written in',
'locale' => '(string) locale code for language blog is written in',
'wga' => '(array) Google Analytics Settings',
'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?',
'disabled_reblogs' => '(bool) Are reblogs disabled on posts?',
'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?',
'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)',
'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"',
'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed',
'sharing_open_links' => '(string) Link target for sharing buttons (same or new)',
'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button',
'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.',
'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site',
'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'',
'gmt_offset' => '(int) Site offset from UTC in hours',
'date_format' => '(string) PHP Date-compatible date format',
'time_format' => '(string) PHP Date-compatible time format',
'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)',
'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site',
'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page',
'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site',
'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page',
Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.',
Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives',
'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook',
'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts',
'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear',
'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache',
'posts_per_page' => '(int) Number of posts to show on blog pages',
'posts_per_rss' => '(int) Number of posts to show in the RSS feed',
'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts',
'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts',
'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments',
),
'response_format' => array(
'updated' => '(array)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
/**
* Site settings v1_2 endpoint.
*/
class WPCOM_JSON_API_Site_Settings_V1_2_Endpoint extends WPCOM_JSON_API_Site_Settings_Endpoint {
/**
* Site format.
*
* @var array
*/
public static $site_format = array(
'ID' => '(int) Site ID',
'name' => '(string) Title of site',
'description' => '(string) Tagline or description of site',
'URL' => '(string) Full URL to the site',
'locale' => '(string) Locale code of the site',
'locale_variant' => '(string) Locale variant code for the site, if set',
'settings' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site.',
);
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
add_filter( 'site_settings_endpoint_update_locale', array( $this, 'update_locale' ) );
add_filter( 'site_settings_endpoint_get', array( $this, 'return_locale' ) );
add_filter( 'site_settings_site_format', array( $this, 'site_format' ) );
return parent::callback( $path, $blog_id );
}
/**
* Get the locale.
*
* @param string $key - the key.
*
* @return string|false
*/
protected function get_locale( $key ) {
if ( 'locale' === $key ) {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
return (string) get_blog_lang_code();
} else {
return get_locale();
}
}
return false;
}
/**
* Return the locale.
*
* @param array $settings - the site settings.
*
* @return array `$settings` with a locale key added.
*/
public function return_locale( $settings ) {
return $settings + array( 'locale' => $this->get_locale( 'locale' ) );
}
/**
* Update the locale.
*
* @param string $value - the locale code.
*
* @return bool
*/
public function update_locale( $value ) {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$lang_id = get_lang_id_by_code( $value );
if ( ! empty( $lang_id ) ) {
if ( update_option( 'lang_id', $lang_id ) ) {
return true;
}
}
}
return false;
}
/**
* Format the site.
*
* @param string $format - the format.
*
* @return array
*/
public function site_format( $format ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return self::$site_format;
}
}
@@ -0,0 +1,139 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Site_Settings_V1_3_Endpoint(
array(
'description' => 'Get detailed settings information about a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.3',
'method' => 'GET',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'context' => false,
),
'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1.3/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
new WPCOM_JSON_API_Site_Settings_V1_3_Endpoint(
array(
'description' => 'Update settings for a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.3',
'method' => 'POST',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'blogname' => '(string) Blog name',
'blogdescription' => '(string) Blog description',
'default_pingback_flag' => '(bool) Notify blogs linked from article?',
'default_ping_status' => '(bool) Allow link notifications from other blogs?',
'default_comment_status' => '(bool) Allow comments on new articles?',
'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines',
'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati',
'jetpack_relatedposts_enabled' => '(bool) Enable related posts?',
'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?',
'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?',
'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?',
'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface',
'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
'jetpack_search_supported' => '(bool) Jetpack Search supported',
'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow',
'infinite_scroll' => '(bool) Support infinite scroll of posts?',
'default_category' => '(int) Default post category',
'default_post_format' => '(string) Default post format',
'require_name_email' => '(bool) Require comment authors to fill out name and email?',
'comment_registration' => '(bool) Require users to be registered and logged in to comment?',
'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?',
'close_comments_days_old' => '(int) Age at which to close comments',
'thread_comments' => '(bool) Enable threaded comments?',
'thread_comments_depth' => '(int) Depth to thread comments',
'page_comments' => '(bool) Break comments into pages?',
'comments_per_page' => '(int) Number of comments to display per page',
'default_comments_page' => '(string) newest|oldest Which page of comments to display first',
'comment_order' => '(string) asc|desc Order to display comments within page',
'comments_notify' => '(bool) Email me when someone comments?',
'moderation_notify' => '(bool) Email me when a comment is helf for moderation?',
'social_notifications_like' => '(bool) Email me when someone likes my post?',
'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?',
'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?',
'comment_moderation' => '(bool) Moderate comments for manual approval?',
'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?',
'comment_max_links' => '(int) Moderate comments that contain X or more links',
'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line',
'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line',
'lang_id' => '(int) ID for language blog is written in',
'locale' => '(string) locale code for language blog is written in',
'wga' => '(array) Google Analytics Settings',
'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?',
'disabled_reblogs' => '(bool) Are reblogs disabled on posts?',
'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?',
'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)',
'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"',
'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed',
'sharing_open_links' => '(string) Link target for sharing buttons (same or new)',
'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button',
'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.',
'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site',
'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'',
'gmt_offset' => '(int) Site offset from UTC in hours',
'date_format' => '(string) PHP Date-compatible date format',
'time_format' => '(string) PHP Date-compatible time format',
'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)',
'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site',
'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page',
'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site',
'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page',
Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.',
Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives',
'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook',
'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts',
'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear',
'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache',
'posts_per_page' => '(int) Number of posts to show on blog pages',
'posts_per_rss' => '(int) Number of posts to show in the RSS feed',
'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts',
'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts',
'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments',
),
'response_format' => array(
'updated' => '(array)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
/**
* Site settings v1_3 endpoint class.
*/
class WPCOM_JSON_API_Site_Settings_V1_3_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_2_Endpoint {
/**
* Get defaults.
*
* @return array
*/
protected function get_defaults() {
return array(
'code' => '',
'anonymize_ip' => false,
'ec_track_purchases' => false,
'ec_track_add_to_cart' => false,
);
}
}
@@ -0,0 +1,185 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Site_Settings_V1_4_Endpoint(
array(
'description' => 'Get detailed settings information about a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.4',
'method' => 'GET',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'query_parameters' => array(
'context' => false,
),
'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1.4/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
new WPCOM_JSON_API_Site_Settings_V1_4_Endpoint(
array(
'description' => 'Update settings for a site.',
'group' => '__do_not_document',
'stat' => 'sites:X',
'min_version' => '1.4',
'method' => 'POST',
'path' => '/sites/%s/settings',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'migration_source_site_domain' => '(string) The source site URL, from the migration flow',
'in_site_migration_flow' => '(string) Whether the site is currently in the Site Migration signup flow.',
'blogname' => '(string) Blog name',
'blogdescription' => '(string) Blog description',
'default_pingback_flag' => '(bool) Notify blogs linked from article?',
'default_ping_status' => '(bool) Allow link notifications from other blogs?',
'default_comment_status' => '(bool) Allow comments on new articles?',
'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines',
'wpcom_data_sharing_opt_out' => '(bool) Did the site opt out of public content sharing with third parties and research partners?',
'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati',
'jetpack_relatedposts_enabled' => '(bool) Enable related posts?',
'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?',
'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?',
'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?',
'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface',
'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
'jetpack_search_supported' => '(bool) Jetpack Search supported',
'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow',
'infinite_scroll' => '(bool) Support infinite scroll of posts?',
'default_category' => '(int) Default post category',
'default_post_format' => '(string) Default post format',
'require_name_email' => '(bool) Require comment authors to fill out name and email?',
'comment_registration' => '(bool) Require users to be registered and logged in to comment?',
'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?',
'close_comments_days_old' => '(int) Age at which to close comments',
'thread_comments' => '(bool) Enable threaded comments?',
'thread_comments_depth' => '(int) Depth to thread comments',
'page_comments' => '(bool) Break comments into pages?',
'comments_per_page' => '(int) Number of comments to display per page',
'default_comments_page' => '(string) newest|oldest Which page of comments to display first',
'comment_order' => '(string) asc|desc Order to display comments within page',
'comments_notify' => '(bool) Email me when someone comments?',
'moderation_notify' => '(bool) Email me when a comment is helf for moderation?',
'social_notifications_like' => '(bool) Email me when someone likes my post?',
'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?',
'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?',
'comment_moderation' => '(bool) Moderate comments for manual approval?',
'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?',
'comment_max_links' => '(int) Moderate comments that contain X or more links',
'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line',
'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line',
'lang_id' => '(int) ID for language blog is written in',
'locale' => '(string) locale code for language blog is written in',
'site_vertical_id' => '(string) The site vertical ID',
'wga' => '(array) Google Analytics Settings',
'jetpack_cloudflare_analytics' => '(array) Cloudflare Analytics Settings',
'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?',
'disabled_reblogs' => '(bool) Are reblogs disabled on posts?',
'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?',
'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)',
'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"',
'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed',
'sharing_open_links' => '(string) Link target for sharing buttons (same or new)',
'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button',
'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.',
'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site',
'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'',
'gmt_offset' => '(int) Site offset from UTC in hours',
'date_format' => '(string) PHP Date-compatible date format',
'time_format' => '(string) PHP Date-compatible time format',
'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)',
'woocommerce_onboarding_profile' => '(array) woocommerce_onboarding_profile',
'woocommerce_store_address' => '(string) woocommerce_store_address option',
'woocommerce_store_address_2' => '(string) woocommerce_store_address_2 option',
'woocommerce_store_city' => '(string) woocommerce_store_city option',
'woocommerce_default_country' => '(string) woocommerce_default_country option',
'woocommerce_store_postcode' => '(string) woocommerce_store_postcode option',
'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site',
'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page',
'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site',
'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page',
Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The SEO meta description for the site.',
Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives',
'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook',
'podcasting_archive' => '(string) The post category, if any, used for publishing podcasts',
'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear',
'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache',
'posts_per_page' => '(int) Number of posts to show on blog pages',
'posts_per_rss' => '(int) Number of posts to show in the RSS feed',
'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts',
'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts',
'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments',
'launchpad_screen' => '(string) Whether or not launchpad is presented and what size it will be',
'wpcom_featured_image_in_email' => '(bool) Whether the Featured image is displayed in the New Post email template or not',
'jetpack_gravatar_in_email' => '(bool) Whether to show author avatar in the email byline',
'jetpack_author_in_email' => '(bool) Whether to show author display name in the email byline',
'jetpack_post_date_in_email' => '(bool) Whether to show date in the email byline',
'wpcom_newsletter_categories' => '(array) Array of post category ids that are marked as newsletter categories',
'wpcom_newsletter_categories_enabled' => '(bool) Whether the newsletter categories are enabled or not',
'sm_enabled' => '(bool) Whether the newsletter Subscribe Modal is enabled or not',
'jetpack_subscribe_overlay_enabled' => '(bool) Whether the newsletter Subscribe Overlay is enabled or not',
'jetpack_subscribe_floating_button_enabled' => '(bool) Whether the newsletter floating subscribe button is enabled or not',
'jetpack_subscriptions_subscribe_post_end_enabled' => '(bool) Whether adding Subscribe block at the end of each post is enabled or not',
'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled or not',
'jetpack_subscriptions_subscribe_navigation_enabled' => '(bool) Whether the Subscribe block navigation placement is enabled or not',
'wpcom_gifting_subscription' => '(bool) Whether gifting is enabled for non auto-renew sites',
'wpcom_reader_views_enabled' => '(bool) Whether showing post views in WordPress.com Reader is enabled for the site',
'wpcom_subscription_emails_use_excerpt' => '(bool) Whether site subscription emails (e.g. New Post email notification) will use post excerpts',
'jetpack_subscriptions_reply_to' => '(string) The reply to email behaviour for newsletter emails',
'jetpack_subscriptions_from_name' => '(string) The from name for newsletter emails',
'show_on_front' => '(string) Whether homepage should display related posts or a static page. The expected value is \'posts\' or \'page\'.',
'page_on_front' => '(string) The page ID of the page to use as the site\'s homepage. It will apply only if \'show_on_front\' is set to \'page\'.',
'page_for_posts' => '(string) The page ID of the page to use as the site\'s posts page. It will apply only if \'show_on_front\' is set to \'page\'.',
'subscription_options' => '(array) Array of three options used in subscription email templates: \'invitation\', \'welcome\' and \'comment_follow\' strings.',
'jetpack_verbum_subscription_modal' => '(bool) Whether Subscription modal is enabled in Verbum comments',
'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt',
'enable_verbum_commenting' => '(bool) Whether Verbum commenting is enabled',
'enable_blocks_comments' => '(bool) Whether blocks comments are enabled',
'highlander_comment_form_prompt' => '(string) The prompt for the comment form',
'jetpack_comment_form_color_scheme' => '(string) The color scheme for the comment form',
'is_fully_managed_agency_site' => '(bool) Whether the site is a fully managed agency site',
),
'response_format' => array(
'updated' => '(array)',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.4/sites/en.blog.wordpress.com/settings?pretty=1',
)
);
/**
* Settings v1_4 endpoint class.
*/
class WPCOM_JSON_API_Site_Settings_V1_4_Endpoint extends WPCOM_JSON_API_Site_Settings_V1_3_Endpoint {
/**
* Get the defaults.
*
* @return array
*/
protected function get_defaults() {
return array(
'code' => '',
'anonymize_ip' => false,
'honor_dnt' => false,
'ec_track_purchases' => false,
'ec_track_add_to_cart' => false,
'enh_ec_tracking' => false,
'enh_ec_track_remove_from_cart' => false,
'enh_ec_track_prod_impression' => false,
'enh_ec_track_prod_click' => false,
'enh_ec_track_prod_detail_view' => false,
'enh_ec_track_checkout_started' => false,
);
}
}
@@ -0,0 +1,268 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new WPCOM_JSON_API_Site_User_Endpoint(
array(
'description' => 'Get details of a user of a site by ID.',
'group' => '__do_not_document', // 'users'
'stat' => 'sites:1:user',
'method' => 'GET',
'path' => '/sites/%s/users/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$user_id' => '(int) User ID',
),
'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/23',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_response' => '{
"ID": 18342963,
"login": "binarysmash",
"email": false,
"name": "binarysmash",
"URL": "http:\/\/binarysmash.wordpress.com",
"avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
"profile_URL": "http:\/\/gravatar.com\/binarysmash",
"roles": [ "administrator" ]
}',
)
);
new WPCOM_JSON_API_Site_User_Endpoint(
array(
'description' => 'Get details of a user of a site by login.',
'group' => 'users',
'stat' => 'sites:1:user',
'method' => 'GET',
'path' => '/sites/%s/users/login:%s',
'path_labels' => array(
'$site' => '(int|string) The site ID or domain.',
'$user_id' => '(string) The user\'s login.',
),
'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/login:binarysmash',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_response' => '{
"ID": 18342963,
"login": "binarysmash",
"email": false,
"name": "binarysmash",
"URL": "http:\/\/binarysmash.wordpress.com",
"avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
"profile_URL": "http:\/\/gravatar.com\/binarysmash",
"roles": [ "administrator" ]
}',
)
);
new WPCOM_JSON_API_Site_User_Endpoint(
array(
'description' => 'Update details of a user of a site.',
'group' => 'users',
'stat' => 'sites:1:user',
'method' => 'POST',
'path' => '/sites/%s/users/%d',
'path_labels' => array(
'$site' => '(int|string) The site ID or domain.',
'$user_id' => '(int) The user\'s ID.',
),
'request_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format,
'response_format' => WPCOM_JSON_API_Site_User_Endpoint::$user_format,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/user/23',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'roles' => array(
array(
'administrator',
),
),
'first_name' => 'Rocco',
'last_name' => 'Tripaldi',
),
),
'example_response' => '{
"ID": 18342963,
"login": "binarysmash",
"email": false,
"name": "binarysmash",
"URL": "http:\/\/binarysmash.wordpress.com",
"avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
"profile_URL": "http:\/\/gravatar.com\/binarysmash",
"roles": [ "administrator" ]
}',
)
);
/**
* Site user endpoint class.
*
* /sites/%s/users/%d -> $blog_id, $user_id
*/
class WPCOM_JSON_API_Site_User_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* User format.
*
* @var array
*/
public static $user_format = array(
'ID' => '(int) The ID of the user',
'login' => '(string) The login username of the user',
'email' => '(string) The email of the user',
'name' => '(string) The name to display for the user',
'first_name' => '(string) The first name of the user',
'last_name' => '(string) The last name of the user',
'nice_name' => '(string) The nice_name to display for the user',
'URL' => '(string) The primary blog of the user',
'avatar_URL' => '(url) Gravatar image URL',
'profile_URL' => '(url) Gravatar Profile URL',
'site_ID' => '(int) ID of the user\'s primary blog',
'roles' => '(array|string) The role or roles of the user',
);
/**
* API Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param int $user_id - the user ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $user_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
// @phan-suppress-next-line PhanDeprecatedFunction -- @todo Switch to current_user_can_for_site when we drop support for WP 6.6.
if ( ! current_user_can_for_blog( $blog_id, 'list_users' ) ) {
return new WP_Error( 'unauthorized', 'User cannot view users for specified site', 403 );
}
// Get the user by ID or login
$get_by = str_contains( $path, '/users/login:' ) ? 'login' : 'id';
$user = get_user_by( $get_by, $user_id );
if ( ! $user ) {
return new WP_Error( 'unknown_user', 'Unknown user', 404 );
}
if ( ! is_user_member_of_blog( $user->ID, $blog_id ) ) {
return new WP_Error( 'unknown_user_for_site', 'Unknown user for site', 404 );
}
if ( 'GET' === $this->api->method ) {
return $this->get_user( $user->ID );
} elseif ( 'POST' === $this->api->method ) {
// @phan-suppress-next-line PhanDeprecatedFunction -- @todo Switch to current_user_can_for_site when we drop support for WP 6.6.
if ( ! current_user_can_for_blog( $blog_id, 'promote_users' ) ) {
return new WP_Error( 'unauthorized_no_promote_cap', 'User cannot promote users for specified site', 403 );
}
return $this->update_user( $user_id, $blog_id );
} else {
return new WP_Error( 'bad_request', 'An unsupported request method was used.' );
}
}
/**
* Get the user.
*
* @param int $user_id - the user ID.
*
* @return object
*/
public function get_user( $user_id ) {
$the_user = $this->get_author( $user_id, true );
if ( $the_user && ! is_wp_error( $the_user ) ) {
$userdata = get_userdata( $user_id );
$the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array();
if ( is_multisite() ) {
$the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' );
}
}
return $the_user;
}
/**
* Updates user data.
*
* @param int $user_id - the user ID.
* @param int $blog_id - the blog ID.
*
* @return array|WP_Error
*/
public function update_user( $user_id, $blog_id ) {
$user = array();
$input = $this->input();
$user['ID'] = $user_id;
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
if ( get_current_user_id() === (int) $user_id && isset( $input['roles'] ) ) {
return new WP_Error( 'unauthorized', 'You cannot change your own role', 403 );
}
if ( $is_wpcom && $user_id !== get_current_user_id() && (int) $user_id === wpcom_get_blog_owner( $blog_id ) ) {
return new WP_Error( 'unauthorized_edit_owner', 'Current user can not edit blog owner', 403 );
}
if ( ! $is_wpcom ) {
foreach ( $input as $key => $value ) {
if ( ! is_array( $value ) ) {
$value = trim( $value );
}
$value = wp_unslash( $value );
switch ( $key ) {
case 'first_name':
case 'last_name':
$user[ $key ] = $value;
break;
case 'display_name':
case 'name':
$user['display_name'] = $value;
break;
}
}
}
if ( isset( $input['roles'] ) ) {
// For now, we only use the first role in the array.
if ( is_array( $input['roles'] ) ) {
$user['role'] = $input['roles'][0];
} elseif ( is_string( $input['roles'] ) ) {
$user['role'] = $input['roles'];
} else {
return new WP_Error( 'invalid_input', __( 'The roles property must be a string or an array.', 'jetpack' ), 400 );
}
$editable_roles = array_keys( get_editable_roles() );
if ( ! in_array( $user['role'], $editable_roles, true ) ) {
return new WP_Error(
'invalid_input',
sprintf(
/* Translators: placeholder is an invalid role name */
esc_html__( '%s is not a valid role.', 'jetpack' ),
$editable_roles
),
400
);
}
}
$result = wp_update_user( $user );
if ( is_wp_error( $result ) ) {
return $result;
}
return $this->get_user( $user_id );
}
}
@@ -0,0 +1,51 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Taxonomy endpoint.
*/
abstract class WPCOM_JSON_API_Taxonomy_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Category object format.
*
* @var array
*/
public $category_object_format = array(
'ID' => '(int) The category ID.',
'name' => '(string) The name of the category.',
'slug' => '(string) The slug of the category.',
'description' => '(string) The description of the category.',
'post_count' => '(int) The number of posts using this category.',
'feed_url' => '(string) The URL of the feed for this category.',
'parent' => '(int) The parent ID for the category.',
'meta' => '(object) Meta data',
);
/**
* Tag object format.
*
* @var array
*/
public $tag_object_format = array(
'ID' => '(int) The tag ID.',
'name' => '(string) The name of the tag.',
'slug' => '(string) The slug of the tag.',
'description' => '(string) The description of the tag.',
'post_count' => '(int) The number of posts using this t.',
'meta' => '(object) Meta data',
);
/**
* Constructor function.
*
* @param string|array|object $args - the arguments.
*/
public function __construct( $args ) {
parent::__construct( $args );
if ( preg_match( '#/tags/#i', $this->path ) ) {
$this->response_format =& $this->tag_object_format;
} else {
$this->response_format =& $this->category_object_format;
}
}
}
@@ -0,0 +1,466 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Manage comments via the WordPress.com REST API.
*
* Endpoints;
* Create a comment on a post: /sites/%s/posts/%d/replies/new
* Create a comment as a reply to another comment: /sites/%s/comments/%d/replies/new
* Edit a comment: /sites/%s/comments/%d
* Delete a comment: /sites/%s/comments/%d/delete
*/
use Automattic\Jetpack\Status;
new WPCOM_JSON_API_Update_Comment_Endpoint(
array(
'description' => 'Create a comment on a post.',
'group' => 'comments',
'stat' => 'posts:1:replies:new',
'method' => 'POST',
'path' => '/sites/%s/posts/%d/replies/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$post_ID' => '(int) The post ID',
),
'request_format' => array(
// explicitly document all input.
'content' => '(HTML) The comment text.',
// @todo Should we open this up to unauthenticated requests too?
// 'author' => '(author object) The author of the comment.',
),
'pass_wpcom_user_details' => true,
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/843/replies/new/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'content' => 'Your reply is very interesting. This is a reply.',
),
),
)
);
new WPCOM_JSON_API_Update_Comment_Endpoint(
array(
'description' => 'Create a comment as a reply to another comment.',
'group' => 'comments',
'stat' => 'comments:1:replies:new',
'method' => 'POST',
'path' => '/sites/%s/comments/%d/replies/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$comment_ID' => '(int) The comment ID',
),
'request_format' => array(
'content' => '(HTML) The comment text.',
// @todo Should we open this up to unauthenticated requests too?
// 'author' => '(author object) The author of the comment.',
),
'pass_wpcom_user_details' => true,
'allow_fallback_to_jetpack_blog_token' => true,
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29/replies/new',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'content' => 'This reply is very interesting. This is editing a comment reply via the API.',
),
),
)
);
new WPCOM_JSON_API_Update_Comment_Endpoint(
array(
'description' => 'Edit a comment.',
'group' => 'comments',
'stat' => 'comments:1:POST',
'method' => 'POST',
'path' => '/sites/%s/comments/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$comment_ID' => '(int) The comment ID',
),
'request_format' => array(
'author' => "(string) The comment author's name.",
'author_email' => "(string) The comment author's email.",
'author_url' => "(string) The comment author's URL.",
'content' => '(HTML) The comment text.',
'date' => "(ISO 8601 datetime) The comment's creation time.",
'status' => array(
'approved' => 'Approve the comment.',
'unapproved' => 'Remove the comment from public view and send it to the moderation queue.',
'spam' => 'Mark the comment as spam.',
'unspam' => 'Unmark the comment as spam. Will attempt to set it to the previous status.',
'trash' => 'Send a comment to the trash if trashing is enabled (see constant: EMPTY_TRASH_DAYS).',
'untrash' => 'Untrash a comment. Only works when the comment is in the trash.',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/29',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'content' => 'This reply is now edited via the API.',
'status' => 'approved',
),
),
)
);
new WPCOM_JSON_API_Update_Comment_Endpoint(
array(
'description' => 'Delete a comment.',
'group' => 'comments',
'stat' => 'comments:1:delete',
'method' => 'POST',
'path' => '/sites/%s/comments/%d/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$comment_ID' => '(int) The comment ID',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/comments/$comment_ID/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Update comments endpoint class.
*/
class WPCOM_JSON_API_Update_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
/**
* WPCOM_JSON_API_Update_Comment_Endpoint constructor.
*
* @param array $args - Args.
*/
public function __construct( $args ) {
parent::__construct( $args );
if ( $this->api->ends_with( $this->path, '/delete' ) ) {
$this->comment_object_format['status']['deleted'] = 'The comment has been deleted permanently.';
}
}
/**
* Update comment API callback.
*
* /sites/%s/posts/%d/replies/new -> $blog_id, $post_id
* /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
* /sites/%s/comments/%d -> $blog_id, $comment_id
* /sites/%s/comments/%d/delete -> $blog_id, $comment_id
*
* @param string $path API path.
* @param int $blog_id The blog ID.
* @param int $object_id The object ID.
*
* @return bool|WP_Error|array
*/
public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
if ( $this->api->ends_with( $path, '/new' ) ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false );
} else {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
}
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( $this->api->ends_with( $path, '/delete' ) ) {
return $this->delete_comment( $path, $blog_id, $object_id );
} elseif ( $this->api->ends_with( $path, '/new' ) ) {
if ( str_contains( $path, '/posts/' ) ) {
return $this->new_comment( $path, $blog_id, $object_id, 0 );
} else {
return $this->new_comment( $path, $blog_id, 0, $object_id );
}
}
return $this->update_comment( $path, $blog_id, $object_id );
}
/**
* Add a new comment to a post or as a reply to another comment.
*
* /sites/%s/posts/%d/replies/new -> $blog_id, $post_id
* /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
*
* @param string $path API path.
* @param int $blog_id The blog ID.
* @param int $post_id The post ID.
* @param int $comment_parent_id The comment parent ID.
*
* @return bool|WP_Error|array
*/
public function new_comment( $path, $blog_id, $post_id, $comment_parent_id ) {
$comment_parent = null;
if ( ! $post_id ) {
$comment_parent = get_comment( $comment_parent_id );
if ( ! $comment_parent_id || ! $comment_parent || is_wp_error( $comment_parent ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
$post_id = $comment_parent->comment_post_ID;
}
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) ) {
return new WP_Error( 'unknown_post', 'Unknown post', 404 );
}
if (
( new Status() )->is_private_site() &&
/**
* Filter allowing non-registered users on the site to comment.
*
* @module json-api
*
* @since 3.4.0
*
* @param bool is_user_member_of_blog() Is the user member of the site.
*/
! apply_filters( 'wpcom_json_api_user_is_member_of_blog', is_user_member_of_blog() ) &&
! is_super_admin()
) {
return new WP_Error( 'unauthorized', 'User cannot create comments', 403 );
}
if ( ! comments_open( $post->ID ) && ! current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error( 'unauthorized', 'Comments on this post are closed', 403 );
}
$can_view = $this->user_can_view_post( $post->ID );
if ( ! $can_view || is_wp_error( $can_view ) ) {
return $can_view;
}
$post_status = get_post_status_object( get_post_status( $post ) );
if ( ! $post_status->public && ! $post_status->private ) {
return new WP_Error( 'unauthorized', 'Comments on drafts are not allowed', 403 );
}
$args = $this->query_args();
$input = $this->input();
if ( ! is_array( $input ) || ! $input || ! strlen( $input['content'] ) ) {
return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
}
$user = wp_get_current_user();
if ( ! $user || is_wp_error( $user ) || ! $user->ID ) {
$auth_required = false;
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$auth_required = true;
} elseif ( isset( $this->api->token_details['user'] ) ) {
$user = (object) $this->api->token_details['user'];
foreach ( array( 'display_name', 'user_email', 'user_url' ) as $user_datum ) {
if ( ! isset( $user->$user_datum ) ) {
$auth_required = true;
}
}
if ( ! isset( $user->ID ) ) {
$user->ID = 0;
}
$author = get_user_by( 'id', (int) $user->ID );
// If we have a user with an external ID saved, we can use it.
if (
! $auth_required
&& $user->ID
&& $author
) {
$user = $author;
}
} else {
$auth_required = true;
}
if ( $auth_required ) {
return new WP_Error( 'authorization_required', 'An active access token must be used to comment.', 403 );
}
}
$insert = array(
'comment_post_ID' => $post->ID,
'user_ID' => $user->ID,
'comment_author' => $user->display_name,
'comment_author_email' => $user->user_email,
'comment_author_url' => $user->user_url,
'comment_content' => $input['content'],
'comment_parent' => $comment_parent_id,
'comment_type' => 'comment',
);
if ( $comment_parent_id ) {
if ( '0' === $comment_parent->comment_approved && current_user_can( 'edit_comment', $comment_parent->comment_ID ) ) {
wp_set_comment_status( $comment_parent->comment_ID, 'approve' );
}
}
$this->api->trap_wp_die( 'comment_failure' );
$comment_id = wp_new_comment( add_magic_quotes( $insert ) );
$this->api->trap_wp_die( null );
$return = $this->get_comment( $comment_id, $args['context'] );
if ( ! $return ) {
return new WP_Error( 400, __( 'Comment cache problem?', 'jetpack' ) );
}
if ( is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'comments' );
return $return;
}
/**
* Update a comment.
*
* /sites/%s/comments/%d -> $blog_id, $comment_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int $comment_id Comment ID.
*
* @return bool|WP_Error|array
*/
public function update_comment( $path, $blog_id, $comment_id ) {
$comment = get_comment( $comment_id );
if ( ! $comment || is_wp_error( $comment ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 );
}
$args = $this->query_args();
$input = $this->input( false );
if ( ! is_array( $input ) || ! $input ) {
return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
}
$update = array();
foreach ( $input as $key => $value ) {
$update[ "comment_$key" ] = $value;
}
$comment_status = wp_get_comment_status( $comment->comment_ID );
if ( isset( $update['comment_status'] ) ) {
switch ( $update['comment_status'] ) {
case 'approved':
if ( 'approve' !== $comment_status ) {
wp_set_comment_status( $comment->comment_ID, 'approve' );
}
break;
case 'unapproved':
if ( 'hold' !== $comment_status ) {
wp_set_comment_status( $comment->comment_ID, 'hold' );
}
break;
case 'spam':
if ( 'spam' !== $comment_status ) {
wp_spam_comment( $comment->comment_ID );
}
break;
case 'unspam':
if ( 'spam' === $comment_status ) {
wp_unspam_comment( $comment->comment_ID );
}
break;
case 'trash':
if ( ! EMPTY_TRASH_DAYS ) {
return new WP_Error( 'trash_disabled', 'Cannot trash comment', 403 );
}
if ( 'trash' !== $comment_status ) {
wp_trash_comment( $comment_id );
}
break;
case 'untrash':
if ( 'trash' === $comment_status ) {
wp_untrash_comment( $comment->comment_ID );
}
break;
default:
$update['comment_approved'] = 1;
break;
}
unset( $update['comment_status'] );
}
if ( ! empty( $update ) ) {
$update['comment_ID'] = $comment->comment_ID;
wp_update_comment( add_magic_quotes( $update ) );
}
$return = $this->get_comment( $comment->comment_ID, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'comments' );
return $return;
}
/**
* Delete a comment.
*
* /sites/%s/comments/%d/delete -> $blog_id, $comment_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int $comment_id Comment ID.
*
* @return bool|WP_Error|array
*/
public function delete_comment( $path, $blog_id, $comment_id ) {
$comment = get_comment( $comment_id );
if ( ! $comment || is_wp_error( $comment ) ) {
return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
}
if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { // [sic] There is no delete_comment cap
return new WP_Error( 'unauthorized', 'User cannot delete comment', 403 );
}
$args = $this->query_args();
$return = $this->get_comment( $comment->comment_ID, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'comments' );
wp_delete_comment( $comment->comment_ID );
$status = wp_get_comment_status( $comment->comment_ID );
if ( false === $status ) {
$return['status'] = 'deleted';
return $return;
}
return $this->get_comment( $comment->comment_ID, $args['context'] );
}
}
@@ -0,0 +1,93 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Custom CSS update endpoint.
*
* Endpoint: /sites/%s/customcss
*/
new WPCOM_JSON_API_Update_CustomCss_Endpoint(
array(
'description' => 'Set custom-css data for a site.',
'group' => '__do_not_document',
'stat' => 'customcss:1:update',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/customcss',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'request_format' => array(
'css' => '(string) Optional. The raw CSS.',
'preprocessor' => '(string) Optional. The name of the preprocessor if any.',
'add_to_existing' => '(bool) Optional. False to skip the existing styles.',
),
'response_format' => array(
'css' => '(string) The raw CSS.',
'preprocessor' => '(string) The name of the preprocessor if any.',
'add_to_existing' => '(bool) False to skip the existing styles.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/12345678/customcss',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'css' => '.stie-title { color: #fff; }',
'preprocessor' => 'sass',
),
),
'example_response' => '
{
"css": ".site-title { color: #fff; }",
"preprocessor": "sass",
"add_to_existing": "true"
}',
)
);
/**
* Custom CSS update endpoint class.
*/
class WPCOM_JSON_API_Update_CustomCss_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Custom CSS update endpoint API callback.
*
* @param string $path API path.
* @param int $blog_id Blog ID.
*
* @return array|WP_Error
*/
public function callback( $path = '', $blog_id = 0 ) {
// Switch to the given blog.
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error( 'unauthorized', 'User is not authorized to access custom css', 403 );
}
$args = $this->input();
if ( empty( $args ) || ! is_array( $args ) ) {
return new WP_Error( 'no_data', 'No data was provided.', 400 );
}
$save_args = array(
'css' => $args['css'],
'preprocessor' => $args['preprocessor'],
'add_to_existing' => $args['add_to_existing'],
);
Jetpack_Custom_CSS::save( $save_args );
$current = array(
'css' => Jetpack_Custom_CSS::get_css(),
'preprocessor' => Jetpack_Custom_CSS::get_preprocessor_key(),
'add_to_existing' => ! Jetpack_Custom_CSS::skip_stylesheet(),
);
$defaults = array(
'css' => '',
'preprocessor' => '',
'add_to_existing' => true,
);
return wp_parse_args( $current, $defaults );
}
}
@@ -0,0 +1,101 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Update media item info endpoint.
*
* Endpoint: /sites/%s/media/%d
*/
new WPCOM_JSON_API_Update_Media_Endpoint(
array(
'description' => 'Edit basic information about a media item.',
'group' => 'media',
'stat' => 'media:1:POST',
'method' => 'POST',
'path' => '/sites/%s/media/%d',
'deprecated' => true,
'max_version' => '1',
'new_version' => '1.1',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'request_format' => array(
'title' => '(string) The file name.',
'caption' => '(string) File caption.',
'description' => '(HTML) Description of the file.',
),
'response_format' => array(
'id' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'parent' => '(int) ID of the post this media is attached to',
'link' => '(string) URL to the file',
'title' => '(string) File name',
'caption' => '(string) User provided caption of the file',
'description' => '(string) Description of the file',
'metadata' => '(array) Array of metadata about the file, such as Exif data or sizes',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/446',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'title' => 'Updated Title',
),
),
)
);
/**
* Update media item info class.
*/
class WPCOM_JSON_API_Update_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Update media item info API callback.
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int $media_id Media ID.
*
* @return object|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'upload_files', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$item = $this->get_media_item( $media_id );
if ( is_wp_error( $item ) ) {
return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
}
$input = $this->input( true );
$insert = array();
if ( ! empty( $input['title'] ) ) {
$insert['post_title'] = $input['title'];
}
if ( ! empty( $input['caption'] ) ) {
$insert['post_excerpt'] = $input['caption'];
}
if ( ! empty( $input['description'] ) ) {
$insert['post_content'] = $input['description'];
}
$insert['ID'] = $media_id;
wp_update_post( (object) $insert );
$item = $this->get_media_item( $media_id );
return $item;
}
}
@@ -0,0 +1,200 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Update media item info v1.1 endpoint.
*
* Endpoint: v1.1/sites/%s/media/%d
*/
new WPCOM_JSON_API_Update_Media_v1_1_Endpoint(
array(
'description' => 'Edit basic information about a media item.',
'group' => 'media',
'stat' => 'media:1:POST',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/media/%d',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$media_ID' => '(int) The ID of the media item',
),
'request_format' => array(
'parent_id' => '(int) ID of the post this media is attached to',
'title' => '(string) The file name.',
'caption' => '(string) File caption.',
'description' => '(HTML) Description of the file.',
'alt' => '(string) Alternative text for image files.',
'rating' => '(string) Video only. Video rating.',
'display_embed' => '(string) Video only. Whether to share or not the video.',
'allow_download' => '(string) Video only. Whether the video can be downloaded or not.',
'privacy_setting' => '(int) Video only. The privacy level for the video.',
'artist' => '(string) Audio Only. Artist metadata for the audio track.',
'album' => '(string) Audio Only. Album metadata for the audio track.',
),
'response_format' => array(
'ID' => '(int) The ID of the media item',
'date' => '(ISO 8601 datetime) The date the media was uploaded',
'post_ID' => '(int) ID of the post this media is attached to',
'author_ID' => '(int) ID of the user who uploaded the media',
'URL' => '(string) URL to the file',
'guid' => '(string) Unique identifier',
'file' => '(string) File name',
'extension' => '(string) File extension',
'mime_type' => '(string) File mime type',
'title' => '(string) File name',
'caption' => '(string) User provided caption of the file',
'description' => '(string) Description of the file',
'alt' => '(string) Alternative text for image files.',
'thumbnails' => '(object) Media item thumbnail URL options',
'height' => '(int) (Image & video only) Height of the media item',
'width' => '(int) (Image & video only) Width of the media item',
'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
'rating' => '(string) (Video only) VideoPress rating of the video',
'display_embed' => '(string) Video only. Whether to share or not the video.',
'allow_download' => '(string) Video only. Whether the video can be downloaded or not.',
'privacy_setting' => '(int) Video only. The privacy level for the video.',
'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/446',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'title' => 'Updated Title',
),
),
)
);
// phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
/**
* Update media item info v1.1 class.
*/
class WPCOM_JSON_API_Update_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Update media item info API v1.1 callback.
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int $media_id Media ID.
*
* @return object|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'upload_files', $media_id ) ) {
return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
}
$item = $this->get_media_item_v1_1( $media_id );
if ( is_wp_error( $item ) ) {
return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
}
$input = $this->input( true );
$insert = array();
if ( isset( $input['title'] ) ) {
$insert['post_title'] = $input['title'];
}
if ( isset( $input['caption'] ) ) {
$insert['post_excerpt'] = $input['caption'];
}
if ( isset( $input['description'] ) ) {
$insert['post_content'] = $input['description'];
}
if ( isset( $input['parent_id'] ) ) {
$insert['post_parent'] = $input['parent_id'];
}
if ( isset( $input['alt'] ) ) {
$alt = wp_strip_all_tags( $input['alt'], true );
update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
}
// audio only artist/album info.
if ( str_starts_with( $item->mime_type, 'audio/' ) ) {
$changed = false;
$id3data = wp_get_attachment_metadata( $media_id );
if ( ! is_array( $id3data ) ) {
$changed = true;
$id3data = array();
}
$id3_keys = array(
'artist' => __( 'Artist', 'jetpack' ),
'album' => __( 'Album', 'jetpack' ),
);
foreach ( $id3_keys as $key => $label ) {
if ( isset( $input[ $key ] ) ) {
$changed = true;
$id3data[ $key ] = wp_strip_all_tags( $input[ $key ], true );
}
}
if ( $changed ) {
wp_update_attachment_metadata( $media_id, $id3data );
}
}
// Pass the item to the handle_video_meta() that checks if it's a VideoPress item and saves it.
$result = $this->handle_video_meta( $media_id, $input, $item );
if ( is_wp_error( $result ) ) {
return $result;
}
$insert['ID'] = $media_id;
wp_update_post( (object) $insert );
$item = $this->get_media_item_v1_1( $media_id );
return $item;
}
/**
* Persist the VideoPress metadata if the given item argument is a VideoPress item.
*
* @param string $media_id The ID of the video.
* @param array $input The request input.
* @param stdClass $item The response item.
*
* @return bool|WP_Error
*/
public function handle_video_meta( $media_id, $input, $item ) {
if ( ! class_exists( \Videopress_Attachment_Metadata::class ) ) {
return false;
}
if ( ! \Videopress_Attachment_Metadata::is_videopress_media( $item ) ) {
return false;
}
return \Videopress_Attachment_Metadata::persist_metadata(
$media_id,
$item->videopress_guid,
isset( $input['title'] ) ? $input['title'] : null,
isset( $input['caption'] ) ? $input['caption'] : null,
isset( $input['description'] ) ? $input['description'] : null,
isset( $input['rating'] ) ? $input['rating'] : null,
isset( $input['display_embed'] ) ? $input['display_embed'] : null,
isset( $input['allow_download'] ) ? $input['allow_download'] : null,
isset( $input['privacy_setting'] ) ? $input['privacy_setting'] : null
);
}
}
@@ -0,0 +1,97 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Set site homepage settings API endpoint.
*
* Endpoint: /sites/%s/homepage
*/
new WPCOM_JSON_API_Update_Site_Homepage_Endpoint(
array(
'description' => 'Set site homepage settings',
'group' => '__do_not_document',
'stat' => 'sites:1:homepage',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/homepage',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'request_format' => array(
'is_page_on_front' => '(bool) True if we will use a page as the homepage; false to use a blog page as the homepage.',
'page_on_front_id' => '(int) Optional. The ID of the page to use as the homepage if is_page_on_front is true.',
'page_for_posts_id' => '(int) Optional. The ID of the page to use as the blog page if is_page_on_front is true.',
),
'response_format' => array(
'is_page_on_front' => '(bool) True if we will use a page as the homepage; false to use a blog page as the homepage.',
'page_on_front_id' => '(int) The ID of the page to use as the homepage if is_page_on_front is true.',
'page_for_posts_id' => '(int) The ID of the page to use as the blog page if is_page_on_front is true.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/homepage',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'is_page_on_front' => true,
'page_on_front_id' => 1,
'page_for_posts_id' => 0,
),
),
'example_response' => '{"is_page_on_front":true,"page_on_front_id":1,"page_for_posts_id":0}',
)
);
/**
* Site homepage setting endpoint class.
*/
class WPCOM_JSON_API_Update_Site_Homepage_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Set site homepage setting API callback.
*
* @param string $path API path.
* @param int $site_id Blog ID.
*/
public function callback( $path = '', $site_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error( 'unauthorized', 'User is not authorized to access homepage settings', 403 );
}
$args = $this->input();
if ( empty( $args ) || ! is_array( $args ) ) {
return $this->get_current_settings();
}
if ( isset( $args['is_page_on_front'] ) ) {
$show_on_front = $args['is_page_on_front'] ? 'page' : 'posts';
update_option( 'show_on_front', $show_on_front );
}
if ( isset( $args['page_on_front_id'] ) ) {
update_option( 'page_on_front', $args['page_on_front_id'] );
}
if ( isset( $args['page_for_posts_id'] ) ) {
update_option( 'page_for_posts', $args['page_for_posts_id'] );
}
return $this->get_current_settings();
}
/**
* Get current site homepage settings.
*
* @return array
*/
public function get_current_settings() {
$is_page_on_front = ( get_option( 'show_on_front' ) === 'page' );
$page_on_front_id = get_option( 'page_on_front' );
$page_for_posts_id = get_option( 'page_for_posts' );
return array(
'is_page_on_front' => $is_page_on_front,
'page_on_front_id' => $page_on_front_id,
'page_for_posts_id' => $page_for_posts_id,
);
}
}
@@ -0,0 +1,117 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Set site logo settings API.
*
* Endpoints:
* Set site logo settings: /sites/%s/logo
* Delete site logo settings: /sites/%s/logo/delete
*/
new WPCOM_JSON_API_Update_Site_Logo_Endpoint(
array(
'description' => 'Set site logo settings',
'group' => '__do_not_document',
'stat' => 'sites:1:logo',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/logo',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'request_format' => array(
'id' => '(int) The ID of the logo post',
'url' => '(string) The URL of the logo post (deprecated)',
),
'response_format' => array(
'id' => '(int) The ID of the logo post',
'url' => '(string) The URL of the logo post',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/logo',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
'body' => array(
'id' => 12345,
),
),
'example_response' => '
{
"id": 12345,
"url": "https:\/\/s.w.org\/about\/images\/logos\/codeispoetry-rgb.png"
}',
)
);
new WPCOM_JSON_API_Update_Site_Logo_Endpoint(
array(
'description' => 'Delete site logo settings',
'group' => '__do_not_document',
'stat' => 'sites:1:logo:delete',
'method' => 'POST',
'min_version' => '1.1',
'path' => '/sites/%s/logo/delete',
'path_labels' => array(
'$site' => '(string) Site ID or domain.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/logo/delete',
'example_request_data' => array(
'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
),
)
);
/**
* Set site logo settings API class.
*/
class WPCOM_JSON_API_Update_Site_Logo_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Set site logo settings API callback.
*
* @param string $path API path.
* @param int $site_id Blog ID.
*/
public function callback( $path = '', $site_id = 0 ) {
// Switch to the given blog.
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error( 'unauthorized', 'User is not authorized to access logo settings', 403 );
}
if ( strpos( $path, '/delete' ) ) {
delete_option( 'site_logo' );
return array();
}
$args = $this->input();
$logo_settings = $this->get_current_settings();
if ( empty( $args ) || ! is_array( $args ) ) {
return $logo_settings;
}
if ( isset( $args['id'] ) ) {
update_option( 'site_logo', (int) $args['id'] );
}
return $this->get_current_settings();
}
/**
* Get current logo settings.
*/
public function get_current_settings() {
$logo_id = get_option( 'site_logo' );
if ( ! $logo_id ) {
return array();
}
return array(
'id' => $logo_id,
'url' => wp_get_attachment_url( $logo_id ),
);
}
}
@@ -0,0 +1,383 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Update site taxonomy API endpoints.
*
* Endpoints:
* Create a new category: /sites/%s/categories/new
* Create a new tag: /sites/%s/tags/new
* Edit a category: /sites/%s/categories/slug:%s
* Edit a tag: /sites/%s/tags/slug:%s
* Delete a category: /sites/%s/categories/slug:%s/delete
* Delete a tag: /sites/%s/tags/slug:%s/delete
*/
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Create a new category.',
'group' => 'taxonomy',
'stat' => 'categories:new',
'method' => 'POST',
'path' => '/sites/%s/categories/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'name' => '(string) Name of the category',
'description' => '(string) A description of the category',
'parent' => '(int) ID of the parent category',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/new/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'name' => 'Puppies',
),
),
)
);
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Create a new tag.',
'group' => 'taxonomy',
'stat' => 'tags:new',
'method' => 'POST',
'path' => '/sites/%s/tags/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'name' => '(string) Name of the tag',
'description' => '(string) A description of the tag',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/new/',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'name' => 'Kitties',
),
),
)
);
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Edit a tag.',
'group' => 'taxonomy',
'stat' => 'tags:1:POST',
'method' => 'POST',
'path' => '/sites/%s/tags/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$tag' => '(string) The tag slug',
),
'request_format' => array(
'name' => '(string) Name of the tag',
'description' => '(string) A description of the tag',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/slug:testing-tag',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'description' => 'Kitties are awesome!',
),
),
)
);
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Edit a category.',
'group' => 'taxonomy',
'stat' => 'categories:1:POST',
'method' => 'POST',
'path' => '/sites/%s/categories/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$category' => '(string) The category slug',
),
'request_format' => array(
'name' => '(string) Name of the category',
'description' => '(string) A description of the category',
'parent' => '(int) ID of the parent category',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/slug:testing-category',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'description' => 'Puppies are great!',
),
),
)
);
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Delete a category.',
'group' => 'taxonomy',
'stat' => 'categories:1:delete',
'method' => 'POST',
'path' => '/sites/%s/categories/slug:%s/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$category' => '(string) The category slug',
),
'response_format' => array(
'slug' => '(string) The slug of the deleted category',
'success' => '(bool) Was the operation successful?',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/categories/slug:$category/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
new WPCOM_JSON_API_Update_Taxonomy_Endpoint(
array(
'description' => 'Delete a tag.',
'group' => 'taxonomy',
'stat' => 'tags:1:delete',
'method' => 'POST',
'path' => '/sites/%s/tags/slug:%s/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$tag' => '(string) The tag slug',
),
'response_format' => array(
'slug' => '(string) The slug of the deleted tag',
'success' => '(bool) Was the operation successful?',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/tags/slug:$tag/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Update site taxonomy API class.
*/
class WPCOM_JSON_API_Update_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
/**
* Update site taxonomy API callback.
*
* - /sites/%s/tags|categories/new -> $blog_id
* - /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id
* - /sites/%s/tags|categories/slug:%s/delete -> $blog_id, $taxonomy_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int|string $object_id Term.
*/
public function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( preg_match( '#/tags/#i', $path ) ) {
$taxonomy_type = 'post_tag';
} else {
$taxonomy_type = 'category';
}
if ( $this->api->ends_with( $path, '/delete' ) ) {
return $this->delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type );
} elseif ( $this->api->ends_with( $path, '/new' ) ) {
return $this->new_taxonomy( $path, $blog_id, $taxonomy_type );
}
return $this->update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type );
}
/**
* Create a new taxonomy.
*
* - /sites/%s/tags|categories/new -> $blog_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param string $taxonomy_type Taxonomy type (category, post_tag).
*/
public function new_taxonomy( $path, $blog_id, $taxonomy_type ) {
$args = $this->query_args();
$input = $this->input();
if ( ! is_array( $input ) || ! $input || ! strlen( $input['name'] ) ) {
return new WP_Error( 'invalid_input', 'Unknown data passed', 400 );
}
$user = wp_get_current_user();
if ( ! $user || is_wp_error( $user ) || ! $user->ID ) {
return new WP_Error( 'authorization_required', 'An active access token must be used to manage taxonomies.', 403 );
}
$tax = get_taxonomy( $taxonomy_type );
if ( ! current_user_can( $tax->cap->edit_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
if ( 'category' !== $taxonomy_type || ! isset( $input['parent'] ) ) {
$input['parent'] = 0;
}
$term = get_term_by( 'name', $input['name'], $taxonomy_type );
if ( $term ) {
// the same name is allowed as long as the parents are different.
if ( $input['parent'] === $term->parent ) {
return new WP_Error( 'duplicate', 'A taxonomy with that name already exists', 400 );
}
}
$data = wp_insert_term(
addslashes( $input['name'] ),
$taxonomy_type,
array(
'description' => isset( $input['description'] ) ? addslashes( $input['description'] ) : '',
'parent' => $input['parent'],
)
);
if ( is_wp_error( $data ) ) {
return $data;
}
$taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type );
$return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'taxonomies' );
return $return;
}
/**
* Update a taxonomy.
*
* - /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int|string $object_id Term.
* @param string $taxonomy_type Taxonomy type (category, post_tag).
*/
public function update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) {
$taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type );
$tax = get_taxonomy( $taxonomy_type );
if ( ! current_user_can( $tax->cap->edit_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
}
if ( false === term_exists( $object_id, $taxonomy_type ) ) {
return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 );
}
$args = $this->query_args();
$input = $this->input( false );
if ( ! is_array( $input ) || ! $input ) {
return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
}
$update = array();
if ( 'category' === $taxonomy_type && ! empty( $input['parent'] ) ) {
$update['parent'] = $input['parent'];
}
if ( ! empty( $input['description'] ) ) {
$update['description'] = addslashes( $input['description'] );
}
if ( ! empty( $input['name'] ) ) {
$update['name'] = addslashes( $input['name'] );
}
$data = wp_update_term( $taxonomy->term_id, $taxonomy_type, $update );
$taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type );
$return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'taxonomies' );
return $return;
}
/**
* Delete a taxonomy.
*
* - /sites/%s/tags|categories/%s/delete -> $blog_id, $taxonomy_id
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int|string $object_id Term.
* @param string $taxonomy_type Taxonomy type (category, post_tag).
*/
public function delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) {
$taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type );
$tax = get_taxonomy( $taxonomy_type );
if ( ! current_user_can( $tax->cap->delete_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
}
if ( false === term_exists( $object_id, $taxonomy_type ) ) {
return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 );
}
$args = $this->query_args();
$return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'taxonomies' );
wp_delete_term( $taxonomy->term_id, $taxonomy_type );
return array(
'slug' => (string) $taxonomy->slug,
'success' => 'true',
);
}
}
@@ -0,0 +1,295 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Update site terms API endpoints.
*
* Endpoints:
* Create a new term: /sites/%s/taxonomies/%s/terms/new
* Edit a term: /sites/%s/taxonomies/%s/terms/slug:%s
* Delete a term: /sites/%s/taxonomies/%s/terms/slug:%s/delete
*/
new WPCOM_JSON_API_Update_Term_Endpoint(
array(
'description' => 'Create a new term.',
'group' => 'taxonomy',
'stat' => 'terms:new',
'method' => 'POST',
'path' => '/sites/%s/taxonomies/%s/terms/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$taxonomy' => '(string) Taxonomy',
),
'request_format' => array(
'name' => '(string) Name of the term',
'description' => '(string) A description of the term',
'parent' => '(int) The parent ID for the term, if hierarchical',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/new',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'name' => 'Ribs & Chicken',
),
),
)
);
new WPCOM_JSON_API_Update_Term_Endpoint(
array(
'description' => 'Edit a term.',
'group' => 'taxonomy',
'stat' => 'terms:1:POST',
'method' => 'POST',
'path' => '/sites/%s/taxonomies/%s/terms/slug:%s',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$taxonomy' => '(string) Taxonomy',
'$slug' => '(string) The term slug',
),
'request_format' => array(
'name' => '(string) Name of the term',
'description' => '(string) A description of the term',
'parent' => '(int) The parent ID for the term, if hierarchical',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/slug:testing-term',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'description' => 'The most delicious',
),
),
)
);
new WPCOM_JSON_API_Update_Term_Endpoint(
array(
'description' => 'Delete a term.',
'group' => 'taxonomy',
'stat' => 'terms:1:delete',
'method' => 'POST',
'path' => '/sites/%s/taxonomies/%s/terms/slug:%s/delete',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
'$taxonomy' => '(string) Taxonomy',
'$slug' => '(string) The term slug',
),
'response_format' => array(
'slug' => '(string) The slug of the deleted term',
'success' => '(bool) Whether the operation was successful',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/taxonomies/post_tag/terms/slug:$term/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
)
);
/**
* Update site terms API endpoint class.
*/
class WPCOM_JSON_API_Update_Term_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
/**
* Update site terms API callback.
*
* - /sites/%s/taxonomies/%s/terms/new -> $blog_id, $taxonomy
* - /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug
* - /sites/%s/taxonomies/%s/terms/slug:%s/delete -> $blog_id, $taxonomy, $slug
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param string $taxonomy Taxonomy.
* @param int|string $slug Slug, term name.
*/
public function callback( $path = '', $blog_id = 0, $taxonomy = 'category', $slug = 0 ) {
$slug = urldecode( $slug );
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->load_theme_functions();
}
$user = wp_get_current_user();
if ( ! $user || is_wp_error( $user ) || ! $user->ID ) {
return new WP_Error( 'authorization_required', 'An active access token must be used to manage taxonomies.', 403 );
}
$taxonomy_meta = get_taxonomy( $taxonomy );
if ( false === $taxonomy_meta || (
! $taxonomy_meta->public &&
! current_user_can( $taxonomy_meta->cap->manage_terms ) &&
! current_user_can( $taxonomy_meta->cap->edit_terms ) &&
! current_user_can( $taxonomy_meta->cap->delete_terms ) ) ) {
return new WP_Error( 'invalid_taxonomy', 'The taxonomy does not exist', 400 );
}
if ( $this->api->ends_with( $path, '/delete' ) ) {
return $this->delete_term( $path, $blog_id, $slug, $taxonomy );
} elseif ( $this->api->ends_with( $path, '/new' ) ) {
return $this->new_term( $path, $blog_id, $taxonomy );
}
return $this->update_term( $path, $blog_id, $slug, $taxonomy );
}
/**
* Create a new term.
*
* - /sites/%s/taxonomies/%s/terms/new -> $blog_id, $taxonomy
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param string $taxonomy Taxonomy.
*/
public function new_term( $path, $blog_id, $taxonomy ) {
$args = $this->query_args();
$input = $this->input();
if ( ! is_array( $input ) || ! $input || ! strlen( $input['name'] ) ) {
return new WP_Error( 'invalid_input', 'Unknown data passed', 400 );
}
$tax = get_taxonomy( $taxonomy );
if ( ! current_user_can( $tax->cap->manage_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
if ( ! isset( $input['parent'] ) || ! is_taxonomy_hierarchical( $taxonomy ) ) {
$input['parent'] = 0;
}
$term = get_term_by( 'name', $input['name'], $taxonomy );
if ( $term ) {
// the same name is allowed as long as the parents are different.
if ( $input['parent'] === $term->parent ) {
return new WP_Error( 'duplicate', 'A taxonomy with that name already exists', 409 );
}
}
$data = wp_insert_term(
addslashes( $input['name'] ),
$taxonomy,
array(
'description' => isset( $input['description'] ) ? addslashes( $input['description'] ) : '',
'parent' => $input['parent'],
)
);
if ( is_wp_error( $data ) ) {
return $data;
}
$term = get_term_by( 'id', $data['term_id'], $taxonomy );
$return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'terms' );
return $return;
}
/**
* Update a term.
*
* - /sites/%s/taxonomies/%s/terms/slug:%s -> $blog_id, $taxonomy, $slug
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int|string $slug Slug, term name.
* @param string $taxonomy Taxonomy.
*/
public function update_term( $path, $blog_id, $slug, $taxonomy ) {
$tax = get_taxonomy( $taxonomy );
if ( ! current_user_can( $tax->cap->edit_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
$term = get_term_by( 'slug', $slug, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
}
$args = $this->query_args();
$input = $this->input( false );
if ( ! is_array( $input ) || ! $input ) {
return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
}
$update = array();
if ( ! empty( $input['parent'] ) || is_taxonomy_hierarchical( $taxonomy ) ) {
$update['parent'] = $input['parent'];
}
if ( isset( $input['description'] ) ) {
$update['description'] = addslashes( $input['description'] );
}
if ( ! empty( $input['name'] ) ) {
$update['name'] = addslashes( $input['name'] );
}
$data = wp_update_term( $term->term_id, $taxonomy, $update );
if ( is_wp_error( $data ) ) {
return $data;
}
$term = get_term_by( 'id', $data['term_id'], $taxonomy );
$return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'terms' );
return $return;
}
/**
* Delete a term.
*
* - /sites/%s/taxonomies/%s/terms/slug:%s/delete -> $blog_id, $taxonomy, $slug
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int|string $slug Slug, term name.
* @param string $taxonomy Taxonomy.
*/
public function delete_term( $path, $blog_id, $slug, $taxonomy ) {
$term = get_term_by( 'slug', $slug, $taxonomy );
$tax = get_taxonomy( $taxonomy );
if ( ! current_user_can( $tax->cap->delete_terms ) ) {
return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
}
if ( ! $term || is_wp_error( $term ) ) {
return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
}
$args = $this->query_args();
$return = $this->get_taxonomy( $term->slug, $taxonomy, $args['context'] );
if ( ! $return || is_wp_error( $return ) ) {
return $return;
}
/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
do_action( 'wpcom_json_api_objects', 'terms' );
wp_delete_term( $term->term_id, $taxonomy );
return array(
'slug' => (string) $term->slug,
'success' => true,
);
}
}
@@ -0,0 +1,195 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Update site users API endpoint.
*
* Endpoint: /sites/%s/users/%d/delete
*/
new WPCOM_JSON_API_Update_User_Endpoint(
array(
'description' => 'Deletes or removes a user of a site.',
'group' => 'users',
'stat' => 'users:delete',
'method' => 'POST',
'path' => '/sites/%s/users/%d/delete',
'path_labels' => array(
'$site' => '(int|string) The site ID or domain.',
'$user_ID' => '(int) The user\'s ID',
),
'request_format' => array(
'reassign' => '(int) An optional id of a user to reassign posts to.',
),
'response_format' => array(
'success' => '(bool) Was the deletion of user successful?',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/users/1/delete',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_response' => '
{
"success": true
}',
)
);
/**
* Update site users API class.
*/
class WPCOM_JSON_API_Update_User_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Update site users API callback.
*
* @param string $path API path.
* @param int $blog_id Blog ID.
* @param int $user_id User ID.
*/
public function callback( $path = '', $blog_id = 0, $user_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( (int) wpcom_get_blog_owner( $blog_id ) === (int) $user_id ) {
return new WP_Error( 'forbidden', 'A site owner can not be removed through this endpoint.', 403 );
}
}
if ( $this->api->ends_with( $path, '/delete' ) ) {
return $this->delete_or_remove_user( $user_id );
}
return false;
}
/**
* Checks if a user exists by checking to see if a WP_User object exists for a user ID.
*
* @param int $user_id User ID.
* @return bool
*/
public function user_exists( $user_id ) {
$user = get_user_by( 'id', $user_id );
return false !== $user && is_a( $user, 'WP_User' );
}
/**
* Return the domain name of a subscription.
*
* @param Store_Subscription $subscription Subscription object.
* @return string
*/
protected function get_subscription_domain_name( $subscription ) {
return $subscription->meta;
}
/**
* Get a list of the domains owned by the given user.
*
* @param int $user_id User ID.
* @return array
*/
protected function domain_subscriptions_for_site_owned_by_user( $user_id ) {
$subscriptions = WPCOM_Store::get_subscriptions( get_current_blog_id(), $user_id, domains::get_domain_products() );
$domains = array_unique( array_map( array( $this, 'get_subscription_domain_name' ), $subscriptions ) );
return array_values( $domains );
}
/**
* Validates user input and then decides whether to remove or delete a user.
*
* @param int $user_id User ID.
* @return array|WP_Error
*/
public function delete_or_remove_user( $user_id ) {
if ( 0 === (int) $user_id ) {
return new WP_Error( 'invalid_input', 'A valid user ID must be specified.', 400 );
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$domains = $this->domain_subscriptions_for_site_owned_by_user( $user_id );
if ( ! empty( $domains ) ) {
$error = new WP_Error( 'user_owns_domain_subscription', implode( ', ', $domains ) );
$error->add_data( $domains, 'additional_data' );
return $error;
}
$active_user_subscriptions = WPCOM_Store::get_user_subscriptions( $user_id, get_current_blog_id() );
if ( ! empty( $active_user_subscriptions ) ) {
$product_names = array_values( wp_list_pluck( $active_user_subscriptions, 'product_name' ) );
$error = new WP_Error( 'user_has_active_subscriptions', 'User has active subscriptions' );
$error->add_data( $product_names, 'additional_data' );
return $error;
}
}
if ( get_current_user_id() === (int) $user_id ) {
return new WP_Error( 'invalid_input', 'User can not remove or delete self through this endpoint.', 400 );
}
if ( ! $this->user_exists( $user_id ) ) {
return new WP_Error( 'invalid_input', 'A user does not exist with that ID.', 400 );
}
return is_multisite() ? $this->remove_user( $user_id ) : $this->delete_user( $user_id );
}
/**
* Removes a user from the current site.
*
* @param int $user_id User ID.
* @return array|WP_Error
*/
public function remove_user( $user_id ) {
if ( ! current_user_can( 'remove_users' ) ) {
return new WP_Error( 'unauthorized', 'User cannot remove users for specified site.', 403 );
}
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return new WP_Error( 'invalid_input', 'User is not a member of the specified site.', 400 );
}
return array(
'success' => remove_user_from_blog( $user_id, get_current_blog_id() ),
);
}
/**
* Deletes a user and optionally reassigns posts to another user.
*
* @param int $user_id User ID.
* @return array|WP_Error
*/
public function delete_user( $user_id ) {
if ( ! current_user_can( 'delete_users' ) ) {
return new WP_Error( 'unauthorized', 'User cannot delete users for specified site.', 403 );
}
$input = (array) $this->input();
if ( isset( $input['reassign'] ) ) {
if ( (int) $user_id === (int) $input['reassign'] ) {
return new WP_Error( 'invalid_input', 'Can not reassign posts to user being deleted.', 400 );
}
if ( ! $this->user_exists( $input['reassign'] ) ) {
return new WP_Error( 'invalid_input', 'User specified in reassign argument is not a member of the specified site.', 400 );
}
}
return array(
'success' => wp_delete_user( $user_id, (int) $input['reassign'] ),
);
}
}
@@ -0,0 +1,116 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Upload media item API endpoint.
*
* Endpoint: /sites/%s/media/new
*/
new WPCOM_JSON_API_Upload_Media_Endpoint(
array(
'description' => 'Upload a new media item.',
'group' => 'media',
'stat' => 'media:new',
'method' => 'POST',
'path' => '/sites/%s/media/new',
'deprecated' => true,
'new_version' => '1.1',
'max_version' => '1',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'media' => '(media) An array of media to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Accepts images (image/gif, image/jpeg, image/png) only at this time.<br /><br /><strong>Example</strong>:<br />' .
"<code>curl \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>",
'media_urls' => '(array) An array of URLs to upload to the post.',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/media/new/',
'response_format' => array(
'media' => '(array) Array of uploaded media',
'errors' => '(array) Array of error messages of uploading media failures',
),
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'media_urls' => 'https://s.w.org/about/images/logos/codeispoetry-rgb.png',
),
),
)
);
/**
* Upload media item API class.
*/
class WPCOM_JSON_API_Upload_Media_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Upload media item API endpoint callback.
*
* @param string $path API path.
* @param int $blog_id Blog ID.
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
}
$input = $this->input( true );
$has_media = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false;
$has_media_urls = isset( $input['media_urls'] ) && $input['media_urls'] ? count( $input['media_urls'] ) : false;
$errors = array();
$files = array();
$media_ids = array();
if ( $has_media ) {
$this->api->trap_wp_die( 'upload_error' );
foreach ( $input['media'] as $index => $media_item ) {
$_FILES['.api.media.item.'] = $media_item;
// check for WP_Error if we ever actually need $media_id.
$media_id = media_handle_upload( '.api.media.item.', 0 );
if ( is_wp_error( $media_id ) ) {
if ( is_countable( $input['media'] ) && 1 === count( $input['media'] ) && ! $has_media_urls ) {
unset( $_FILES['.api.media.item.'] );
return $media_id;
}
$errors[ $index ]['error'] = $media_id->get_error_code();
$errors[ $index ]['message'] = $media_id->get_error_message();
} else {
$media_ids[ $index ] = $media_id;
}
$files[] = $media_item;
}
$this->api->trap_wp_die( null );
unset( $_FILES['.api.media.item.'] );
}
if ( $has_media_urls ) {
foreach ( $input['media_urls'] as $url ) {
$id = $this->handle_media_sideload( $url );
if ( ! empty( $id ) && is_int( $id ) ) {
$media_ids[] = $id;
}
}
}
$results = array();
foreach ( $media_ids as $media_id ) {
$results[] = $this->get_media_item( $media_id );
}
return array(
'media' => $results,
'errors' => $errors,
);
}
}
@@ -0,0 +1,287 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Upload media item API endpoint v1.1
*
* Endpoint: /sites/%s/media/new
*/
new WPCOM_JSON_API_Upload_Media_v1_1_Endpoint(
array(
'description' => 'Upload a new piece of media.',
'allow_cross_origin_request' => true,
'allow_upload_token_auth' => true,
'group' => 'media',
'stat' => 'media:new',
'min_version' => '1.1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/media/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'media' => '(media) An array of media to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint.<br /><br /><strong>Example</strong>:<br />' .
"<code>curl \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>",
'media_urls' => '(array) An array of URLs to upload to the post. Errors produced by media uploads, if any, will be in `media_errors` in the response.',
'attrs' => '(array) An array of attributes (`title`, `description`, `caption` `alt` for images, `artist` for audio, `album` for audio, and `parent_id`) are supported to assign to the media uploaded via the `media` or `media_urls` properties. You must use a numeric index for the keys of `attrs` which follows the same sequence as `media` and `media_urls`. <br /><br /><strong>Example</strong>:<br />' .
"<code>curl \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://example.com/file2.jpg' \<br /> \<br />--form 'attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>",
),
'response_format' => array(
'media' => '(array) Array of uploaded media objects',
'errors' => '(array) Array of error messages of uploading media failures',
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/media/new',
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'media_urls' => 'https://s.w.org/about/images/logos/codeispoetry-rgb.png',
),
),
)
);
// phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
/**
* Upload media item API class v1.1
*/
class WPCOM_JSON_API_Upload_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Upload media item API endpoint callback v1.1
*
* @param string $path API path.
* @param int $blog_id Blog ID.
*
* @return array|int|WP_Error|void
*/
public function callback( $path = '', $blog_id = 0 ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
if ( ! current_user_can( 'upload_files' ) && ! $this->api->is_authorized_with_upload_token() ) {
return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
}
$input = $this->input( true );
$media_files = ! empty( $input['media'] ) ? $input['media'] : array();
$media_urls = ! empty( $input['media_urls'] ) ? $input['media_urls'] : array();
$media_attrs = ! empty( $input['attrs'] ) ? $input['attrs'] : array();
if ( empty( $media_files ) && empty( $media_urls ) ) {
return new WP_Error( 'invalid_input', 'No media provided in input.' );
}
$jetpack_sync = null;
$is_jetpack_site = false;
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// For jetpack sites, we send the media via a different method, because the sync is very different.
$jetpack_sync = Jetpack_Media_Sync::summon( $blog_id );
$is_jetpack_site = $jetpack_sync->is_jetpack_site();
}
$jetpack_media_files = array();
$other_media_files = array();
$media_items = array();
$errors = array();
// We're splitting out videos for Jetpack sites.
foreach ( $media_files as $media_item ) {
if ( isset( $media_item['type'] ) && preg_match( '@^video/@', $media_item['type'] ) && $is_jetpack_site ) {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM &&
defined( 'VIDEOPRESS_JETPACK_VIDEO_ENABLED' ) && VIDEOPRESS_JETPACK_VIDEO_ENABLED
) {
// Check that video upload space is available for a Jetpack site (skipped if site is Atomic).
$result = videopress_check_space_available_for_jetpack( $blog_id, $media_item['name'], $media_item['size'] );
if ( true !== $result ) {
$this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( array( $result ) ) ) );
continue;
}
}
$jetpack_media_files[] = $media_item;
} else {
$other_media_files[] = $media_item;
}
}
// New Jetpack / VideoPress media upload processing.
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( is_countable( $jetpack_media_files ) && count( $jetpack_media_files ) > 0 ) {
add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
// get_space_used() checks blog upload directory storage,
// so filter it temporarily to return only video storage used.
add_filter( 'pre_get_space_used', 'videopress_filter_jetpack_get_space_used' );
$media_items = $jetpack_sync->upload_media( $jetpack_media_files, $this->api );
$errors = $jetpack_sync->get_errors();
foreach ( $media_items as & $media_item ) {
// More than likely a post has not been created yet, so we pass in the media item we
// got back from the Jetpack site.
$post = (object) $media_item['post'];
$media_item = $this->get_media_item_v1_1( $post->ID, $post, $media_item['file'] );
}
// Remove get_space_used filter after upload.
remove_filter( 'pre_get_space_used', 'videopress_filter_jetpack_get_space_used' );
}
}
// Normal WPCOM upload processing.
if ( ( is_countable( $other_media_files ) && count( $other_media_files ) > 0 ) || ( is_countable( $other_media_files ) && count( $media_urls ) > 0 ) ) {
if ( is_multisite() ) { // Do not check for available space in non multisites.
add_filter( 'wp_handle_upload_prefilter', array( $this, 'check_upload_size' ), 9 ); // used for direct media uploads.
add_filter( 'wp_handle_sideload_prefilter', array( $this, 'check_upload_size' ), 9 ); // used for uploading media via url.
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
require_lib( 'tos-acceptance-tracking' );
add_filter( 'wp_handle_upload_prefilter', '\\A8C\\TOS_Acceptance_Tracking\\handle_uploads_wpcomtos_blog' );
}
$create_media = $this->handle_media_creation_v1_1( $other_media_files, $media_urls, $media_attrs );
$media_ids = $create_media['media_ids'];
$errors = $create_media['errors'];
$media_items = array();
foreach ( $media_ids as $media_id ) {
$media_items[] = $this->get_media_item_v1_1( $media_id );
}
}
if ( array() === $media_items ) {
return $this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( $errors ) ) );
}
$results = array();
foreach ( $media_items as $media_item ) {
if ( is_wp_error( $media_item ) ) {
$errors[] = array(
'file' => $media_item['ID'],
'error' => $media_item->get_error_code(),
'message' => $media_item->get_error_message(),
);
} else {
$results[] = $media_item;
}
}
$response = array( 'media' => $results );
if ( is_countable( $errors ) && count( $errors ) > 0 ) {
$response['errors'] = $this->rewrite_generic_upload_error( $errors );
}
return $response;
}
/**
* This changes the generic "upload_error" code to something more meaningful if possible
*
* @param array $errors Errors for the uploaded file.
* @return array The same array with an improved error message.
*/
public function rewrite_generic_upload_error( $errors ) {
foreach ( $errors as $k => $error ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( 'upload_error' === $error['error'] && str_contains( $error['message'], '|' ) ) {
list( $errors[ $k ]['error'], $errors[ $k ]['message'] ) = explode( '|', $error['message'], 2 ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}
}
return $errors;
}
/**
* Determine if uploaded file exceeds space quota on multisite.
*
* This is a copy of the core function with added functionality, synced
* with this with WP_REST_Attachments_Controller::check_upload_size()
* to allow for specifying a better error message.
*
* @param array $file $_FILES array for a given file.
* @return array Maybe extended with an error message.
*/
public function check_upload_size( $file ) {
if ( get_site_option( 'upload_space_check_disabled' ) ) {
return $file;
}
if ( isset( $file['error'] ) && $file['error'] > 0 ) { // There's already an error. Error Codes Reference: https://www.php.net/manual/en/features.file-upload.errors.php .
return $file;
}
// We don't know if this is an upload or a sideload, but in either case the tmp_name should be a path, not a URL.
if ( wp_parse_url( $file['tmp_name'], PHP_URL_SCHEME ) !== null ) {
$file['error'] = 'rest_upload_invalid|' . __( 'Specified file failed upload test.', 'default' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
return $file;
}
if ( defined( 'WP_IMPORTING' ) ) {
return $file;
}
$space_left = get_upload_space_available();
$file_size = filesize( $file['tmp_name'] );
if ( $space_left < $file_size ) {
/* translators: %s: Required disk space in kilobytes. */
$file['error'] = 'rest_upload_limited_space|' . sprintf( __( 'Not enough space to upload. %s KB needed.', 'default' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
}
$max_upload_size = KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 );
if ( defined( 'IS_WPCOM' ) && IS_WPCOM && defined( 'WPCOM_MAX_UPLOAD_FILE_SIZE' ) ) {
$max_upload_size = WPCOM_MAX_UPLOAD_FILE_SIZE;
}
if ( $file_size > $max_upload_size ) {
/* translators: %s: Maximum allowed file size in kilobytes. */
$file['error'] = 'rest_upload_file_too_big|' . sprintf( __( 'This file is too big. Files must be less than %s KB in size.', 'jetpack' ), $max_upload_size / KB_IN_BYTES );
}
if ( upload_is_user_over_quota( false ) ) {
$file['error'] = 'rest_upload_user_quota_exceeded|' . __( 'You have used your space quota. Please delete files before uploading.', 'default' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
}
return $file;
}
/**
* Force to use the WPCOM API instead of proxy back to the Jetpack API if the blog is a paid Jetpack
* blog w/ the VideoPress module enabled AND the uploaded file is a video.
*
* @param int $blog_id Blog ID.
* @return bool
*/
public function force_wpcom_request( $blog_id ) {
// We don't need to do anything if VideoPress is not enabled for the blog.
if ( ! is_videopress_enabled_on_jetpack_blog( $blog_id ) ) {
return false;
}
// Check to see if the upload is not a video type, if not then return false.
$input = $this->input( true );
$media_files = ! empty( $input['media'] ) ? $input['media'] : array();
if ( empty( $media_files ) ) {
return false;
}
foreach ( $media_files as $media_item ) {
if ( ! isset( $media_item['type'] ) || ! preg_match( '@^video/@', $media_item['type'] ) ) {
return false;
}
}
// The API request should be for a blog w/ Jetpack, A valid plan, has VideoPress enabled,
// and is a video file. Let's let it through.
return true;
}
}
@@ -0,0 +1,83 @@
<?php
/**
* API endpoint /sites/%s/delete-backup-helper-script
* This API endpoint deletes a Jetpack Backup Helper Script
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Backup\V0005\Helper_Script_Manager;
/**
* API endpoint /sites/%s/delete-backup-helper-script
* This API endpoint deletes a Jetpack Backup Helper Script
*/
class Jetpack_JSON_API_Delete_Backup_Helper_Script_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* This endpoint is only accessible from Jetpack Backup; it requires no further capabilities.
*
* @var array
*/
protected $needed_capabilities = array();
/**
* Method to call when running this endpoint (delete)
*
* @var string
*/
protected $action = 'delete';
/**
* Local path to the Helper Script to delete.
*
* @var string|null
*/
protected $script_path = null;
/**
* An array with 'success' => true if the specified file has been successfully deleted, or an instance of WP_Error.
*
* @var array|WP_Error
*/
protected $result;
/**
* Checks that the input args look like a valid Helper Script path.
*
* @param null $object Unused.
* @return bool|WP_Error a WP_Error object or true if the input seems ok.
*/
protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$args = $this->input();
if ( ! isset( $args['path'] ) ) {
return new WP_Error( 'invalid_args', __( 'You must specify a helper script path', 'jetpack' ), 400 );
}
$this->script_path = $args['path'];
return true;
}
/**
* Deletes the specified Helper Script.
*/
protected function delete() {
$delete_result = Helper_Script_Manager::delete_helper_script( $this->script_path );
Helper_Script_Manager::cleanup_expired_helper_scripts();
if ( is_wp_error( $delete_result ) ) {
$this->result = $delete_result;
} else {
$this->result = array( 'success' => true );
}
}
/**
* Returns the success or failure of the deletion operation
*
* @return array An array containing one key; 'success', which specifies whether the operation was successful.
*/
protected function result() {
return $this->result;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* API endpoint /sites/%s/install-backup-helper-script
* This API endpoint installs a Helper Script to assist Jetpack Backup fetch data
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Backup\V0005\Helper_Script_Manager;
/**
* API endpoint /sites/%s/install-backup-helper-script
* This API endpoint installs a Helper Script to assist Jetpack Backup fetch data
*/
class Jetpack_JSON_API_Install_Backup_Helper_Script_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* This endpoint is only accessible from Jetpack Backup; it requires no further capabilities.
*
* @var array
*/
protected $needed_capabilities = array();
/**
* Method to call when running this endpoint (install)
*
* @var string
*/
protected $action = 'install';
/**
* Contents of the Helper Script to install
*
* @var string|null
*/
protected $helper_script = null;
/**
* Contains the result of installing the Helper Script.
*
* @var null|WP_Error|array
*/
protected $result = null;
/**
* Checks that the input args look like a valid Helper Script.
*
* @param null $object Unused.
* @return bool|WP_Error a WP_Error object or true if the input seems ok.
*/
protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$args = $this->input();
if ( ! isset( $args['helper'] ) ) {
return new WP_Error( 'invalid_args', __( 'You must specify a helper script body', 'jetpack' ), 400 );
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$this->helper_script = base64_decode( $args['helper'] );
if ( ! $this->helper_script ) {
return new WP_Error( 'invalid_args', __( 'Helper script body must be base64 encoded', 'jetpack' ), 400 );
}
return true;
}
/**
* Installs the uploaded Helper Script.
*/
protected function install() {
$this->result = Helper_Script_Manager::install_helper_script( $this->helper_script );
Helper_Script_Manager::cleanup_expired_helper_scripts();
}
/**
* Return the success or failure of the backup helper script installation operation.
*
* @return array|WP_Error An array with installation info on success:
*
* 'path' (string) Helper script installation path on the filesystem.
* 'url' (string) URL to the helper script.
* 'abspath' (string) WordPress root.
*
* or an instance of WP_Error on failure.
*/
protected function result() {
return $this->result;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* List modules v1.2 endpoint.
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Status;
/**
* List modules v1.2 endpoint.
*/
class Jetpack_JSON_API_Modules_List_V1_2_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* This endpoint allows authentication both via a blog and a user token.
* If a user token is used, that user should have `jetpack_manage_modules` capability.
*
* @var array|string
*/
protected $needed_capabilities = 'jetpack_manage_modules';
/**
* Fetch modules list.
*
* @return array An array of module objects.
*/
protected function result() {
require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
$is_offline_mode = ( new Status() )->is_offline_mode();
$modules = Jetpack_Admin::init()->get_modules();
foreach ( $modules as $slug => $properties ) {
if ( $is_offline_mode ) {
$requires_connection = isset( $modules[ $slug ]['requires_connection'] ) && $modules[ $slug ]['requires_connection'];
$requires_user_connection = isset( $modules[ $slug ]['requires_user_connection'] ) && $modules[ $slug ]['requires_user_connection'];
if (
$requires_connection || $requires_user_connection
) {
$modules[ $slug ]['activated'] = false;
}
}
}
$modules = Jetpack::get_translated_modules( $modules );
return array( 'modules' => $modules );
}
}
@@ -0,0 +1,39 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Check capabilities endpoint class.
*
* GET /sites/%s/me/capability
*/
class Jetpack_JSON_API_Check_Capabilities_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
/**
*
* API callback.
*
* @param string $path - the path.
* @param int $_blog_id - the blog ID.
* @param object $object - parameter is for making the method signature compatible with its parent class method.
* @return bool|bool[]|WP_Error
*/
public function callback( $path = '', $_blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// Check minimum capability and blog membership first
$error = $this->validate_call( $_blog_id, 'read', false );
if ( is_wp_error( $error ) ) {
return $error;
}
$args = $this->input();
if ( ! isset( $args['capability'] ) || empty( $args['capability'] ) ) {
return new WP_Error( 'missing_capability', __( 'You are required to specify a capability to check.', 'jetpack' ), 400 );
}
$capability = $args['capability'];
if ( is_array( $capability ) ) {
$results = array_map( 'current_user_can', $capability );
return array_combine( $capability, $results );
} else {
return current_user_can( $capability );
}
}
}
@@ -0,0 +1,46 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Core endpoint class.
*
* POST /sites/%s/core
* POST /sites/%s/core/update
*/
class Jetpack_JSON_API_Core_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'manage_options';
/**
* New version.
*
* @var string
*/
protected $new_version;
/**
* An array of log strings.
*
* @var array
*/
protected $log;
/**
* Return the result of the wp_version.
*
* @return array
*/
public function result() {
global $wp_version;
return array(
'version' => ( empty( $this->new_version ) ) ? $wp_version : $this->new_version,
'autoupdate' => Jetpack_Options::get_option( 'autoupdate_core', false ),
'log' => $this->log,
);
}
}
@@ -0,0 +1,118 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Core modify endpoint class.
*
* POST /sites/%s/core
* POST /sites/%s/core/update
*/
class Jetpack_JSON_API_Core_Modify_Endpoint extends Jetpack_JSON_API_Core_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'update_core';
/**
* Action.
*
* @var string
*/
protected $action = 'default_action';
/**
* New version.
*
* @var string
*/
protected $new_version;
/**
* An array of log strings.
*
* @var array
*/
protected $log;
/**
* The default action.
*
* @return bool
*/
public function default_action() {
$args = $this->input();
if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) {
Jetpack_Options::update_option( 'autoupdate_core', $args['autoupdate'] );
}
return true;
}
/**
* Update the version.
*
* @return string|false|WP_Error New WordPress version on success, false or WP_Error on failure.
*/
protected function update() {
$args = $this->input();
$version = isset( $args['version'] ) ? $args['version'] : false;
$locale = isset( $args['locale'] ) ? $args['locale'] : get_locale();
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
delete_site_transient( 'update_core' );
wp_version_check( array(), true );
if ( $version ) {
$update = find_core_update( $version, $locale );
} else {
$update = $this->find_latest_update_offer();
}
/**
* Pre-upgrade action
*
* @since 3.9.3
*
* @param object|array $update as returned by find_core_update() or find_core_auto_update()
*/
do_action( 'jetpack_pre_core_upgrade', $update );
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Core_Upgrader( $skin );
$this->new_version = $upgrader->upgrade( $update );
$this->log = $upgrader->skin->get_upgrade_messages();
if ( is_wp_error( $this->new_version ) ) {
return $this->new_version;
}
return $this->new_version;
}
/**
* Select the latest update.
* Remove filters to bypass automattic updates.
*
* @return object|false The core update offering on success, false on failure.
*/
protected function find_latest_update_offer() {
// Select the latest update.
// Remove filters to bypass automattic updates.
add_filter( 'request_filesystem_credentials', '__return_true' );
add_filter( 'automatic_updates_is_vcs_checkout', '__return_false' );
add_filter( 'allow_major_auto_core_updates', '__return_true' );
add_filter( 'send_core_update_notification_email', '__return_false' );
$update = find_core_auto_update();
remove_filter( 'request_filesystem_credentials', '__return_true' );
remove_filter( 'automatic_updates_is_vcs_checkout', '__return_false' );
remove_filter( 'allow_major_auto_core_updates', '__return_true' );
remove_filter( 'send_core_update_notification_email', '__return_false' );
return $update;
}
}
@@ -0,0 +1,333 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Cron endpoint class.
*
* GET /sites/%s/cron
*/
class Jetpack_JSON_API_Cron_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'manage_options';
/**
* Validate the call.
*
* @param int $_blog_id - the blog ID.
* @param array $capability - the capabilities of the user.
* @param bool $check_manage_active - parameter is for making the method signature compatible with its parent class method.
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return parent::validate_call( $_blog_id, $capability, false );
}
/**
* Return the result of current timestamp.
*/
protected function result() {
return array(
'cron_array' => _get_cron_array(),
'current_timestamp' => time(),
);
}
/**
* Sanitize the hook.
*
* @param string $hook - the hook.
*
* @return string
*/
protected function sanitize_hook( $hook ) {
return preg_replace( '/[^A-Za-z0-9-_]/', '', $hook );
}
/**
* Resolve arguments.
*
* @return array
*/
protected function resolve_arguments() {
$args = $this->input();
return isset( $args['arguments'] ) ? json_decode( $args['arguments'] ) : array();
}
/**
* Check the cron lock.
*
* @param float $gmt_time - the time in GMT.
*
* @return string|int|WP_Error WP_Error if cron was locked in the `WP_CRON_LOCK_TIMEOUT` seconds before `gmt_time`, int or string otherwise.
*/
protected function is_cron_locked( $gmt_time ) {
// The cron lock: a unix timestamp from when the cron was spawned.
$doing_cron_transient = $this->get_cron_lock();
if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) ) {
return new WP_Error( 'cron-is-locked', 'Current there is a cron already happening.', 403 );
}
return $doing_cron_transient;
}
/**
* Check if we can unlock the cron transient.
*
* @param string $doing_wp_cron - if we're doing the wp_cron.
*/
protected function maybe_unlock_cron( $doing_wp_cron ) {
if ( $this->get_cron_lock() === $doing_wp_cron ) {
delete_transient( 'doing_cron' );
}
}
/**
* Set the cron lock.
*
* @return string
*/
protected function lock_cron() {
$lock = sprintf( '%.22F', microtime( true ) );
set_transient( 'doing_cron', $lock );
return $lock;
}
/**
* Get scheduled.
*
* @param string $hook - the hook.
* @param array $args - the arguments.
*
* @return array
*/
protected function get_schedules( $hook, $args ) {
$crons = _get_cron_array();
$key = md5( serialize( $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
if ( empty( $crons ) ) {
return array();
}
$found = array();
foreach ( $crons as $timestamp => $cron ) {
if ( isset( $cron[ $hook ][ $key ] ) ) {
$found[] = $timestamp;
}
}
return $found;
}
/**
* This function is based on the one found in wp-cron.php with a similar name
*
* @return int
*/
protected function get_cron_lock() {
global $wpdb;
$value = 0;
if ( wp_using_ext_object_cache() ) {
/*
* Skip local cache and force re-fetch of doing_cron transient
* in case another process updated the cache.
*/
$value = wp_cache_get( 'doing_cron', 'transient', true );
} else {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
if ( is_object( $row ) ) {
$value = $row->option_value;
}
}
return $value;
}
}
/**
* Cron post endpoint class.
*
* POST /sites/%s/cron
*/
class Jetpack_JSON_API_Cron_Post_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
define( 'DOING_CRON', true );
set_time_limit( 0 );
$args = $this->input();
$crons = _get_cron_array();
if ( false === $crons ) {
return new WP_Error( 'no-cron-event', 'Currently there are no cron events', 400 );
}
$timestamps_to_run = array_keys( $crons );
$gmt_time = microtime( true );
if ( isset( $timestamps_to_run[0] ) && $timestamps_to_run[0] > $gmt_time ) {
return new WP_Error( 'no-cron-event', 'Currently there are no cron events ready to be run', 400 );
}
$locked = $this->is_cron_locked( $gmt_time );
if ( is_wp_error( $locked ) ) {
return $locked;
}
$lock = $this->lock_cron();
$processed_events = array();
foreach ( $crons as $timestamp => $cronhooks ) {
if ( $timestamp > $gmt_time && ! isset( $args['hook'] ) ) {
break;
}
foreach ( $cronhooks as $hook => $hook_data ) {
if ( isset( $args['hook'] ) && ! in_array( $hook, $args['hook'], true ) ) {
continue;
}
foreach ( $hook_data as $hook_item ) {
$schedule = $hook_item['schedule'];
$arguments = $hook_item['args'];
if ( ! $schedule ) {
wp_reschedule_event( $timestamp, $schedule, $hook, $arguments );
}
wp_unschedule_event( $timestamp, $hook, $arguments );
do_action_ref_array( $hook, $arguments );
$processed_events[] = array( $hook => $arguments );
// If the hook ran too long and another cron process stole the lock,
// or if we things are taking longer then 20 seconds then quit.
if ( ( $this->get_cron_lock() !== $lock ) || ( $gmt_time + 20 > microtime( true ) ) ) {
$this->maybe_unlock_cron( $lock );
return array( 'success' => $processed_events );
}
}
}
}
$this->maybe_unlock_cron( $lock );
return array( 'success' => $processed_events );
}
}
/**
* Schedule endpoint class.
*
* POST /sites/%s/cron/schedule
*/
class Jetpack_JSON_API_Cron_Schedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->input();
if ( ! isset( $args['timestamp'] ) ) {
return new WP_Error( 'missing_argument', 'Please provide the timestamp argument', 400 );
}
if ( ! is_int( $args['timestamp'] ) || $args['timestamp'] < time() ) {
return new WP_Error( 'timestamp-invalid', 'Please provide timestamp that is an integer and set in the future', 400 );
}
if ( ! isset( $args['hook'] ) ) {
return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
}
$hook = $this->sanitize_hook( $args['hook'] );
$locked = $this->is_cron_locked( microtime( true ) );
if ( is_wp_error( $locked ) ) {
return $locked;
}
$arguments = $this->resolve_arguments();
$next_scheduled = $this->get_schedules( $hook, $arguments );
if ( isset( $args['recurrence'] ) ) {
$schedules = wp_get_schedules();
if ( ! isset( $schedules[ $args['recurrence'] ] ) ) {
return new WP_Error( 'invalid-recurrence', 'Please provide a valid recurrence argument', 400 );
}
if ( is_countable( $next_scheduled ) && count( $next_scheduled ) > 0 ) {
return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
}
$lock = $this->lock_cron();
wp_schedule_event( $args['timestamp'], $args['recurrence'], $hook, $arguments );
$this->maybe_unlock_cron( $lock );
return array( 'success' => true );
}
foreach ( $next_scheduled as $scheduled_time ) {
if ( abs( $scheduled_time - $args['timestamp'] ) <= 10 * MINUTE_IN_SECONDS ) {
return new WP_Error( 'event-already-scheduled', 'This event is ready scheduled', 400 );
}
}
$lock = $this->lock_cron();
$next = wp_schedule_single_event( $args['timestamp'], $hook, $arguments );
$this->maybe_unlock_cron( $lock );
return array( 'success' => $next );
}
}
/**
* The cron unschedule ednpoint class.
*
* POST /sites/%s/cron/unschedule
*/
class Jetpack_JSON_API_Cron_Unschedule_Endpoint extends Jetpack_JSON_API_Cron_Endpoint { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->input();
if ( ! isset( $args['hook'] ) ) {
return new WP_Error( 'missing_argument', 'Please provide the hook argument', 400 );
}
$hook = $this->sanitize_hook( $args['hook'] );
$locked = $this->is_cron_locked( microtime( true ) );
if ( is_wp_error( $locked ) ) {
return $locked;
}
$crons = _get_cron_array();
if ( empty( $crons ) ) {
return new WP_Error( 'cron-not-present', 'Unable to unschedule an event, no events in the cron', 400 );
}
$arguments = $this->resolve_arguments();
if ( isset( $args['timestamp'] ) ) {
$next_schedulded = $this->get_schedules( $hook, $arguments );
if ( in_array( $args['timestamp'], $next_schedulded, true ) ) {
return new WP_Error( 'event-not-present', 'Unable to unschedule the event, the event doesn\'t exist', 400 );
}
$lock = $this->lock_cron();
wp_unschedule_event( $args['timestamp'], $hook, $arguments );
$this->maybe_unlock_cron( $lock );
return array( 'success' => true );
}
$lock = $this->lock_cron();
wp_clear_scheduled_hook( $hook, $arguments );
$this->maybe_unlock_cron( $lock );
return array( 'success' => true );
}
}
@@ -0,0 +1,177 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
require JETPACK__PLUGIN_DIR . '/modules/module-info.php';
/**
* Base class for Jetpack Endpoints, has the validate_call helper function.
*/
abstract class Jetpack_JSON_API_Endpoint extends WPCOM_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities;
/**
* Expected actions.
*
* @var array
*/
protected $expected_actions = array();
/**
* The action.
*
* @var string
*/
protected $action;
/**
* Callback function.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param object $object - parameter is for making the method signature compatible with its parent class method.
*/
public function callback( $path = '', $blog_id = 0, $object = null ) {
$error = $this->validate_call( $blog_id, $this->needed_capabilities );
if ( is_wp_error( $error ) ) {
return $error;
}
$error = $this->validate_input( $object );
if ( is_wp_error( $error ) ) {
return $error;
}
if ( ! empty( $this->action ) ) {
$error = call_user_func( array( $this, $this->action ) );
if ( is_wp_error( $error ) ) {
return $error;
}
}
return $this->result();
}
/**
* The result function.
*/
abstract protected function result();
/**
* Validate input.
*
* @param object $object - unused, for parent class compatability.
*
* @return bool
*/
protected function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$args = $this->input();
if ( isset( $args['action'] ) && $args['action'] === 'update' ) {
$this->action = 'update';
}
if ( preg_match( '!/update/?$!', $this->path ) ) {
$this->action = 'update';
} elseif ( preg_match( '/\/install\/?$/', $this->path ) ) {
$this->action = 'install';
} elseif ( ! empty( $args['action'] ) ) {
if ( ! in_array( $args['action'], $this->expected_actions, true ) ) {
return new WP_Error( 'invalid_action', __( 'You must specify a valid action', 'jetpack' ) );
}
$this->action = $args['action'];
}
return true;
}
/**
* Switches to the blog and checks current user capabilities.
*
* @param int $_blog_id - the blog ID.
* @param array $capability - the capabilities of the user.
* @param bool $check_validation - if we're checking the validation.
*
* @return bool|WP_Error a WP_Error object or true if things are good.
*/
protected function validate_call( $_blog_id, $capability, $check_validation = true ) {
$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $_blog_id ) );
if ( is_wp_error( $blog_id ) ) {
return $blog_id;
}
$error = $this->check_capability( $capability );
if ( is_wp_error( $error ) ) {
return $error;
}
if (
$check_validation &&
'GET' !== $this->method &&
/**
* Filter to disallow JSON API requests to the site.
* Setting to false disallows you to manage your site remotely from WordPress.com
* and disallows plugin auto-updates.
*
* @since 7.3.0
*
* @param bool $check_validation Whether to allow API requests to manage the site
*/
! apply_filters( 'jetpack_json_manage_api_enabled', $check_validation )
) {
return new WP_Error( 'unauthorized_full_access', __( 'Full management mode is off for this site.', 'jetpack' ), 403 );
}
return true;
}
/**
* Check capability.
*
* @param array $capability - the compatability.
*
* @return bool|WP_Error
*/
protected function check_capability( $capability ) {
// If this endpoint accepts site based authentication, skip capabilities check.
if ( $this->accepts_site_based_authentication() ) {
return true;
}
if ( is_array( $capability ) ) {
// the idea is that the we can pass in an array of capabilitie that the user needs to have before we allowing them to do something
$capabilities = ( isset( $capability['capabilities'] ) ? $capability['capabilities'] : $capability );
// We can pass in the number of conditions we must pass by default it is all.
$must_pass = ( isset( $capability['must_pass'] ) && is_int( $capability['must_pass'] ) ? $capability['must_pass'] : count( $capabilities ) );
$failed = array(); // store the failed capabilities
$passed = 0;
foreach ( $capabilities as $cap ) {
if ( current_user_can( $cap ) ) {
++$passed;
} else {
$failed[] = $cap;
}
}
// Check if all conditions have passed.
if ( $passed < $must_pass ) {
return new WP_Error(
'unauthorized',
/* translators: %s: comma-separated list of capabilities */
sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), implode( ', ', $failed ) ),
403
);
}
} elseif ( ! current_user_can( $capability ) ) {
// Translators: the capability that the user is not authorized for.
return new WP_Error( 'unauthorized', sprintf( __( 'This user is not authorized to %s on this blog.', 'jetpack' ), $capability ), 403 );
}
return true;
}
}
@@ -0,0 +1,81 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* The Get comment backup endpoint class.
*
* /sites/%s/comments/%d/backup -> $blog_id, $comment_id
*/
class Jetpack_JSON_API_Get_Comment_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* The comment ID.
*
* @var int
*/
protected $comment_id;
/**
* Validate input
*
* @param int $comment_id - the comment ID.
*
* @return bool|WP_Error
*/
public function validate_input( $comment_id ) {
if ( empty( $comment_id ) || ! is_numeric( $comment_id ) ) {
return new WP_Error( 'comment_id_not_specified', __( 'You must specify a Comment ID', 'jetpack' ), 400 );
}
$this->comment_id = (int) $comment_id;
return true;
}
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$comment = get_comment( $this->comment_id );
if ( empty( $comment ) ) {
return new WP_Error( 'comment_not_found', __( 'Comment not found', 'jetpack' ), 404 );
}
$allowed_keys = array(
'comment_ID',
'comment_post_ID',
'comment_author',
'comment_author_email',
'comment_author_url',
'comment_author_IP',
'comment_date',
'comment_date_gmt',
'comment_content',
'comment_karma',
'comment_approved',
'comment_agent',
'comment_type',
'comment_parent',
'user_id',
);
$comment = array_intersect_key( $comment->to_array(), array_flip( $allowed_keys ) );
$comment_meta = get_comment_meta( $comment['comment_ID'] );
return array(
'comment' => $comment,
'meta' => is_array( $comment_meta ) ? $comment_meta : array(),
);
}
}
@@ -0,0 +1,136 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Get Database object backup endpoint class.
*
* /sites/%s/database-object/backup -> $blog_id
*/
class Jetpack_JSON_API_Get_Database_Object_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* Object type.
*
* @var string
*/
protected $object_type;
/**
* Object ID.
*
* @var int
*/
protected $object_id;
/**
* Full list of database objects that can be retrieved via this endpoint.
*
* @var array
*/
protected $object_types = array(
'woocommerce_attribute' => array(
'table' => 'woocommerce_attribute_taxonomies',
'id_field' => 'attribute_id',
),
'woocommerce_downloadable_product_permission' => array(
'table' => 'woocommerce_downloadable_product_permissions',
'id_field' => 'permission_id',
),
'woocommerce_order_item' => array(
'table' => 'woocommerce_order_items',
'id_field' => 'order_item_id',
'meta_type' => 'order_item',
),
'woocommerce_payment_token' => array(
'table' => 'woocommerce_payment_tokens',
'id_field' => 'token_id',
'meta_type' => 'payment_token',
),
'woocommerce_tax_rate' => array(
'table' => 'woocommerce_tax_rates',
'id_field' => 'tax_rate_id',
'child_table' => 'woocommerce_tax_rate_locations',
'child_id_field' => 'tax_rate_id',
),
'woocommerce_webhook' => array(
'table' => 'wc_webhooks',
'id_field' => 'webhook_id',
),
);
/**
* Validate input.
*
* @param object $object - unused.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$query_args = $this->query_args();
if ( empty( $query_args['object_type'] ) || empty( $query_args['object_id'] ) ) {
return new WP_Error( 'invalid_args', __( 'You must specify both an object type and id to fetch', 'jetpack' ), 400 );
}
if ( empty( $this->object_types[ $query_args['object_type'] ] ) ) {
return new WP_Error( 'invalid_args', __( 'Specified object_type not recognized', 'jetpack' ), 400 );
}
$this->object_type = $this->object_types[ $query_args['object_type'] ];
$this->object_id = $query_args['object_id'];
return true;
}
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
global $wpdb;
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$table = $wpdb->prefix . $this->object_type['table'];
$id_field = $this->object_type['id_field'];
// Fetch the requested object
$query = $wpdb->prepare( 'select * from `' . $table . '` where `' . $id_field . '` = %d', $this->object_id ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$object = $wpdb->get_row( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( empty( $object ) ) {
return new WP_Error( 'object_not_found', __( 'Object not found', 'jetpack' ), 404 );
}
$result = array( 'object' => $object );
// Fetch associated metadata (if this object type has any)
if ( ! empty( $this->object_type['meta_type'] ) ) {
$result['meta'] = get_metadata( $this->object_type['meta_type'], $this->object_id );
}
// If there is a child linked table (eg: woocommerce_tax_rate_locations), fetch linked records
if ( ! empty( $this->object_type['child_table'] ) ) {
$child_table = $wpdb->prefix . $this->object_type['child_table'];
$child_id_field = $this->object_type['child_id_field'];
$query = $wpdb->prepare( 'select * from `' . $child_table . '` where `' . $child_id_field . '` = %d', $this->object_id ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result['children'] = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
return $result;
}
}
@@ -0,0 +1,68 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Get option backup endpoint.
*
* /sites/%s/options/backup -> $blog_id
*/
class Jetpack_JSON_API_Get_Option_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* Option names.
*
* @var string
*/
protected $option_names;
/**
* Validate input.
*
* @param object $object - unused.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$query_args = $this->query_args();
if ( empty( $query_args['name'] ) ) {
return new WP_Error( 'option_name_not_specified', __( 'You must specify an option name', 'jetpack' ), 400 );
}
if ( is_array( $query_args['name'] ) ) {
$this->option_names = $query_args['name'];
} else {
$this->option_names = array( $query_args['name'] );
}
return true;
}
/**
* The result.
*/
protected function result() {
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$options = array_map( array( $this, 'get_option_row' ), $this->option_names );
return array( 'options' => $options );
}
/**
* Get options row.
*
* @param string $name - name of the row.
*
* @return object|null Database query result or null on failure.
*/
private function get_option_row( $name ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) );
}
}
@@ -0,0 +1,69 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Get post backup endpoint class.
*
* /sites/%s/posts/%d/backup -> $blog_id, $post_id
*/
class Jetpack_JSON_API_Get_Post_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* The post ID.
*
* @var int
*/
protected $post_id;
/**
* Validate the input.
*
* @param int $post_id - the post ID.
*/
public function validate_input( $post_id ) {
if ( empty( $post_id ) || ! is_numeric( $post_id ) ) {
return new WP_Error( 'post_id_not_specified', __( 'You must specify a Post ID', 'jetpack' ), 400 );
}
$this->post_id = (int) $post_id;
return true;
}
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
global $wpdb;
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$post = get_post( $this->post_id );
if ( empty( $post ) ) {
return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack' ), 404 );
}
// Fetch terms associated with this post object
$terms = $wpdb->get_results(
$wpdb->prepare(
"SELECT term_taxonomy_id, term_order FROM {$wpdb->term_relationships} WHERE object_id = %d;",
$post->ID
)
);
return array(
'post' => (array) $post,
'meta' => get_post_meta( $post->ID ),
'terms' => (array) $terms,
);
}
}
@@ -0,0 +1,60 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Get Term backup endpoint class.
*
* /sites/%s/terms/%d/backup -> $blog_id, $term_id
*/
class Jetpack_JSON_API_Get_Term_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* The term ID.
*
* @var int
*/
protected $term_id;
/**
* Validate input.
*
* @param int $term_id - the term ID.
*
* @return bool|WP_Error
*/
public function validate_input( $term_id ) {
if ( empty( $term_id ) || ! is_numeric( $term_id ) ) {
return new WP_Error( 'term_id_not_specified', __( 'You must specify a Term ID', 'jetpack' ), 400 );
}
$this->term_id = (int) $term_id;
return true;
}
/**
* Return the result.
*
* @return array|WP_Error
*/
protected function result() {
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$term = get_term( $this->term_id );
if ( empty( $term ) ) {
return new WP_Error( 'term_not_found', __( 'Term not found', 'jetpack' ), 404 );
}
return array(
'term' => (array) $term,
'meta' => get_term_meta( $this->term_id ),
);
}
}
@@ -0,0 +1,60 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Get user Backup endpoint class.
*
* /sites/%s/users/%d/backup -> $blog_id, $user_id
*/
class Jetpack_JSON_API_Get_User_Backup_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array(); // This endpoint is only accessible using a site token
/**
* The user ID.
*
* @var int
*/
protected $user_id;
/**
* Validate input.
*
* @param int $user_id - the user ID.
*
* @return bool|WP_Error
*/
public function validate_input( $user_id ) {
if ( empty( $user_id ) || ! is_numeric( $user_id ) ) {
return new WP_Error( 'user_id_not_specified', __( 'You must specify a User ID', 'jetpack' ), 400 );
}
$this->user_id = (int) $user_id;
return true;
}
/**
* The result.
*
* @return array|WP_Error
*/
protected function result() {
// Disable Sync as this is a read-only operation and triggered by sync activity.
\Automattic\Jetpack\Sync\Actions::mark_sync_read_only();
$user = get_user_by( 'id', $this->user_id );
if ( empty( $user ) ) {
return new WP_Error( 'user_not_found', __( 'User not found', 'jetpack' ), 404 );
}
return array(
'user' => $user->to_array(),
'meta' => get_user_meta( $user->ID ),
);
}
}
@@ -0,0 +1,78 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* JPS WooCommerce connect endpoint.
*/
class Jetpack_JSON_API_JPS_WooCommerce_Connect_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'manage_options';
/**
* The result.
*
* @return array|WP_Error
*/
public function result() {
$input = $this->input();
$helper_data = get_option( 'woocommerce_helper_data', array() );
if ( ! empty( $helper_data['auth'] ) ) {
return new WP_Error(
'already_configured',
__( 'WooCommerce auth data is already set.', 'jetpack' )
);
}
// Only update the auth field for `woocommerce_helper_data` instead of blowing out the entire option.
$helper_data['auth'] = array(
'user_id' => $input['user_id'],
'site_id' => $input['site_id'],
'updated' => time(),
'access_token' => $input['access_token'],
'access_token_secret' => $input['access_token_secret'],
);
$updated = update_option(
'woocommerce_helper_data',
$helper_data
);
return array(
'success' => $updated,
);
}
/**
* Validate input.
*
* @param object $object - the object we're validating.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) {
$input = $this->input();
if ( empty( $input['access_token'] ) ) {
return new WP_Error( 'input_error', __( 'access_token is required', 'jetpack' ) );
}
if ( empty( $input['access_token_secret'] ) ) {
return new WP_Error( 'input_error', __( 'access_token_secret is required', 'jetpack' ) );
}
if ( empty( $input['user_id'] ) ) {
return new WP_Error( 'input_error', __( 'user_id is required', 'jetpack' ) );
}
if ( empty( $input['site_id'] ) ) {
return new WP_Error( 'input_error', __( 'site_id is required', 'jetpack' ) );
}
return parent::validate_input( $object );
}
}
@@ -0,0 +1,30 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Jetpack log endpoint class.
*
* GET /sites/%s/jetpack-log
*/
class Jetpack_JSON_API_Jetpack_Log_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = 'manage_options';
/**
* The result.
*
* @return array
*/
protected function result() {
$args = $this->input();
$event = ( isset( $args['event'] ) && is_string( $args['event'] ) ) ? $args['event'] : false;
$num = ( isset( $args['num'] ) ) ? (int) $args['num'] : false;
return array(
'log' => Jetpack::get_log( $event, $num ),
);
}
}
@@ -0,0 +1,56 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Auto update endpoint class.
*
* POST /sites/%s/maybe_auto_update
*/
class Jetpack_JSON_API_Maybe_Auto_Update_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' );
/**
* Update results.
*
* @var array
*/
protected $update_results = array();
/**
* The result.
*
* @return array
*/
protected function result() {
add_action( 'automatic_updates_complete', array( $this, 'get_update_results' ), 100, 1 );
wp_maybe_auto_update();
$result = array();
$result['log'] = $this->update_results;
if ( empty( $result['log'] ) ) {
$possible_reasons_for_failure = Jetpack_Autoupdate::get_possible_failures();
if ( $possible_reasons_for_failure ) {
$result['log']['error'] = $possible_reasons_for_failure;
}
}
return $result;
}
/**
* Get update results.
*
* @param array $results - the results.
*/
public function get_update_results( $results ) {
$this->update_results = $results;
}
}
@@ -0,0 +1,157 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Base class for working with Jetpack Modules.
*/
abstract class Jetpack_JSON_API_Modules_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* The modules.
*
* @var array
*/
protected $modules = array();
/**
* If we're working in bulk.
*
* @var boolean
*/
protected $bulk = true;
/**
* Response format.
*
* @var array
*/
public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
'id' => '(string) The module\'s ID',
'active' => '(boolean) The module\'s status.',
'name' => '(string) The module\'s name.',
'description' => '(safehtml) The module\'s description.',
'sort' => '(int) The module\'s display order.',
'introduced' => '(string) The Jetpack version when the module was introduced.',
'changed' => '(string) The Jetpack version when the module was changed.',
'free' => '(boolean) The module\'s Free or Paid status.',
'module_tags' => '(array) The module\'s tags.',
'override' => '(string) The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
);
/**
* The result.
*
* @return array
*/
protected function result() {
$modules = $this->get_modules();
if ( ! $this->bulk && ! empty( $modules ) ) {
return array_pop( $modules );
}
return array( 'modules' => $modules );
}
/**
* Walks through either the submitted modules or list of themes and creates the global array.
*
* @param string $module - the modules.
*
* @return bool|WP_Error
*/
protected function validate_input( $module ) {
$args = $this->input();
// lets set what modules were requested, and validate them
if ( ! isset( $module ) || empty( $module ) ) {
if ( ! $args['modules'] || empty( $args['modules'] ) ) {
return new WP_Error( 'missing_module', __( 'You are required to specify a module.', 'jetpack' ), 400 );
}
if ( is_array( $args['modules'] ) ) {
$this->modules = $args['modules'];
} else {
$this->modules[] = $args['modules'];
}
} else {
$this->modules[] = urldecode( $module );
$this->bulk = false;
}
$error = $this->validate_modules();
if ( is_wp_error( $error ) ) {
return $error;
}
return parent::validate_input( $module );
}
/**
* Walks through submitted themes to make sure they are valid
*
* @return bool|WP_Error
*/
protected function validate_modules() {
foreach ( $this->modules as $module ) {
if ( ! Jetpack::is_module( $module ) ) {
// Translators: the module that's not found.
return new WP_Error( 'unknown_jetpack_module', sprintf( __( 'Module not found: `%s`.', 'jetpack' ), $module ), 404 );
}
}
return true;
}
/**
* Format the module.
*
* @param string $module_slug - the module slug.
*
* @return array
*/
protected static function format_module( $module_slug ) {
$module_data = Jetpack::get_module( $module_slug );
$module = array();
$module['id'] = $module_slug;
$module['active'] = Jetpack::is_module_active( $module_slug );
$module['name'] = $module_data['name'];
$module['short_description'] = $module_data['description'];
$module['sort'] = $module_data['sort'];
$module['introduced'] = $module_data['introduced'];
$module['changed'] = $module_data['changed'];
$module['free'] = $module_data['free'];
$module['module_tags'] = array_map( 'jetpack_get_module_i18n_tag', $module_data['module_tags'] );
$overrides_instance = Jetpack_Modules_Overrides::instance();
$module['override'] = $overrides_instance->get_module_override( $module_slug );
// Fetch the HTML formatted long description
ob_start();
/** This action is documented in class.jetpack-modules-list-table.php */
do_action( 'jetpack_module_more_info_' . $module_slug );
$module['description'] = ob_get_clean();
return $module;
}
/**
* Format a list of modules for public display, using the supplied offset and limit args
*
* @uses WPCOM_JSON_API_Endpoint::query_args()
* @return array Public API modules objects
*/
protected function get_modules() {
$modules = array_values( $this->modules );
// do offset & limit - we've already returned a 400 error if they're bad numbers
$args = $this->query_args();
if ( isset( $args['offset'] ) ) {
$modules = array_slice( $modules, (int) $args['offset'] );
}
if ( isset( $args['limit'] ) ) {
$modules = array_slice( $modules, 0, (int) $args['limit'] );
}
return array_map( array( $this, 'format_module' ), $modules );
}
}
@@ -0,0 +1,15 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* The Modules get endpoint.
*
* /sites/%s/jetpack/modules/%s
*/
class Jetpack_JSON_API_Modules_Get_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'jetpack_manage_modules';
}
@@ -0,0 +1,28 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileNames
/**
* Modules list endpoint.
*
* GET /sites/%s/jetpack/modules
*/
class Jetpack_JSON_API_Modules_List_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'jetpack_manage_modules';
/**
* Validate the input.
*
* @param string $module - the module.
*
* @return bool
*/
public function validate_input( $module ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$this->modules = Jetpack::get_available_modules();
return true;
}
}
@@ -0,0 +1,92 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Modules modify endpoint class.
*
* POST /sites/%s/jetpack/modules/%s/activate
* POST /sites/%s/jetpack/modules/%s
* POST /sites/%s/jetpack/modules
*/
class Jetpack_JSON_API_Modules_Modify_Endpoint extends Jetpack_JSON_API_Modules_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'activate_plugins';
/**
* The action.
*
* @var string
*/
protected $action = 'default_action';
/**
* The default action.
*/
public function default_action() {
$args = $this->input();
if ( isset( $args['active'] ) && is_bool( $args['active'] ) ) {
if ( $args['active'] ) {
return $this->activate_module();
} else {
return $this->deactivate_module();
}
}
return true;
}
/**
* Activate module.
*
* @return bool|WP_Error
*/
protected function activate_module() {
foreach ( $this->modules as $module ) {
if ( Jetpack::is_module_active( $module ) ) {
$error = __( 'The Jetpack Module is already activated.', 'jetpack' );
$this->log[ $module ][] = $error;
continue;
}
$result = Jetpack::activate_module( $module, false, false );
if ( false === $result || ! Jetpack::is_module_active( $module ) ) {
$error = __( 'There was an error while activating the module.', 'jetpack' );
$this->log[ $module ][] = $error;
}
}
if ( ! $this->bulk && isset( $error ) ) {
return new WP_Error( 'activation_error', $error, 400 );
}
return true;
}
/**
* Deactivate module.
*
* @return bool|WP_Error
*/
protected function deactivate_module() {
foreach ( $this->modules as $module ) {
if ( ! Jetpack::is_module_active( $module ) ) {
$error = __( 'The Jetpack Module is already deactivated.', 'jetpack' );
$this->log[ $module ] = $error;
continue;
}
$result = Jetpack::deactivate_module( $module );
if ( false === $result || Jetpack::is_module_active( $module ) ) {
$error = __( 'There was an error while deactivating the module.', 'jetpack' );
$this->log[ $module ] = $error;
}
}
if ( ! $this->bulk && isset( $error ) ) {
return new WP_Error( 'deactivation_error', $error, 400 );
}
return true;
}
}
@@ -0,0 +1,102 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
// POST /sites/%s/plugins/%s/delete
new Jetpack_JSON_API_Plugins_Delete_Endpoint(
array(
'description' => 'Delete/Uninstall a plugin from your jetpack blog',
'group' => '__do_not_document',
'stat' => 'plugins:1:delete',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/delete',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(int|string) The plugin slug to delete',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/akismet%2Fakismet/delete',
)
);
// v1.2
new Jetpack_JSON_API_Plugins_Delete_Endpoint(
array(
'description' => 'Delete/Uninstall a plugin from your jetpack blog',
'group' => '__do_not_document',
'stat' => 'plugins:1:delete',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/delete',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(int|string) The plugin slug to delete',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/akismet%2Fakismet/delete',
)
);
/**
* Plugins delete endpoint class.
*
* POST /sites/%s/plugins/%s/delete
*/
class Jetpack_JSON_API_Plugins_Delete_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'delete_plugins';
/**
* The action.
*
* @var string
*/
protected $action = 'delete';
/**
* The delete function.
*
* @return bool|WP_Error
*/
protected function delete() {
foreach ( $this->plugins as $plugin ) {
if ( Jetpack::is_plugin_active( $plugin ) ) {
$error = __( 'You cannot delete a plugin while it is active on the main site.', 'jetpack' );
$this->log[ $plugin ][] = $error;
continue;
}
$result = delete_plugins( array( $plugin ) );
if ( is_wp_error( $result ) ) {
$error = $result->get_error_message();
$this->log[ $plugin ][] = $error;
} else {
$this->log[ $plugin ][] = 'Plugin deleted';
}
}
if ( ! $this->bulk && isset( $error ) ) {
return new WP_Error( 'delete_plugin_error', $error, 400 );
}
return true;
}
}
@@ -0,0 +1,497 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\Sync\Functions;
/**
* Base class for working with plugins.
*/
abstract class Jetpack_JSON_API_Plugins_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Plugins.
*
* @var array
*/
protected $plugins = array();
/**
* If the plugin is network wide.
*
* @var boolean
*/
protected $network_wide = false;
/**
* If we're working in bulk.
*
* @var boolean
*/
protected $bulk = true;
/**
* The log.
*
* @var array
*/
protected $log;
/**
* If the request is a scheduled update.
*
* @var boolean
*/
protected $scheduled_update = false;
/**
* Response format.
*
* @var array
*/
public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
'id' => '(safehtml) The plugin\'s ID',
'slug' => '(safehtml) The plugin\'s .org slug',
'active' => '(boolean) The plugin status.',
'update' => '(object) The plugin update info.',
'name' => '(safehtml) The name of the plugin.',
'plugin_url' => '(url) Link to the plugin\'s web site.',
'version' => '(safehtml) The plugin version number.',
'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
'author' => '(safehtml) The author\'s name',
'author_url' => '(url) The authors web site address',
'network' => '(boolean) Whether the plugin can only be activated network wide.',
'autoupdate' => '(boolean) Whether the plugin is automatically updated',
'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations',
'next_autoupdate' => '(string) Y-m-d H:i:s for next scheduled update event',
'log' => '(array:safehtml) An array of update log strings.',
'uninstallable' => '(boolean) Whether the plugin is unistallable.',
'action_links' => '(array) An array of action links that the plugin uses.',
);
/**
* Response format v1_2
*
* @var array
*/
public static $_response_format_v1_2 = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
'slug' => '(safehtml) The plugin\'s .org slug',
'active' => '(boolean) The plugin status.',
'update' => '(object) The plugin update info.',
'name' => '(safehtml) The plugin\'s ID',
'display_name' => '(safehtml) The name of the plugin.',
'version' => '(safehtml) The plugin version number.',
'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
'author' => '(safehtml) The author\'s name',
'author_url' => '(url) The authors web site address',
'plugin_url' => '(url) Link to the plugin\'s web site.',
'network' => '(boolean) Whether the plugin can only be activated network wide.',
'autoupdate' => '(boolean) Whether the plugin is automatically updated',
'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations',
'uninstallable' => '(boolean) Whether the plugin is unistallable.',
'action_links' => '(array) An array of action links that the plugin uses.',
'log' => '(array:safehtml) An array of update log strings.',
);
/**
* The result.
*
* @return array
*/
protected function result() {
$plugins = $this->get_plugins();
if ( ! $this->bulk && ! empty( $plugins ) ) {
return array_pop( $plugins );
}
return array( 'plugins' => $plugins );
}
/**
* Validate the input.
*
* @param string $plugin - the plugin we're validating.
*
* @return bool|WP_Error
*/
protected function validate_input( $plugin ) {
$error = parent::validate_input( $plugin );
if ( is_wp_error( $error ) ) {
return $error;
}
$error = $this->validate_network_wide();
if ( is_wp_error( $error ) ) {
return $error;
}
$error = $this->validate_scheduled_update();
if ( is_wp_error( $error ) ) {
return $error;
}
$args = $this->input();
// find out what plugin, or plugins we are dealing with
// validate the requested plugins
if ( ! isset( $plugin ) || empty( $plugin ) ) {
if ( ! $args['plugins'] || empty( $args['plugins'] ) ) {
return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin.', 'jetpack' ), 400 );
}
if ( is_array( $args['plugins'] ) ) {
$this->plugins = $args['plugins'];
} else {
$this->plugins[] = $args['plugins'];
}
} else {
$this->bulk = false;
$this->plugins[] = urldecode( $plugin );
}
$error = $this->validate_plugins();
if ( is_wp_error( $error ) ) {
return $error;
}
return true;
}
/**
* Walks through submitted plugins to make sure they are valid
*
* @return bool|WP_Error
*/
protected function validate_plugins() {
if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) {
return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) );
}
foreach ( $this->plugins as $index => $plugin ) {
if ( ! preg_match( '/\.php$/', $plugin ) ) {
$plugin = $plugin . '.php';
$this->plugins[ $index ] = $plugin;
}
$valid = $this->validate_plugin( urldecode( $plugin ) );
if ( is_wp_error( $valid ) ) {
return $valid;
}
}
return true;
}
/**
* Format the plugin.
*
* @param string $plugin_file - the plugin file.
* @param array $plugin_data - the plugin data.
*
* @return array
*/
protected function format_plugin( $plugin_file, $plugin_data ) {
if ( version_compare( $this->min_version, '1.2', '>=' ) ) {
return $this->format_plugin_v1_2( $plugin_file, $plugin_data );
}
$plugin = array();
$plugin['id'] = preg_replace( '/(.+)\.php$/', '$1', $plugin_file );
$plugin['slug'] = Jetpack_Autoupdate::get_plugin_slug( $plugin_file );
$plugin['active'] = Jetpack::is_plugin_active( $plugin_file );
$plugin['name'] = $plugin_data['Name'];
$plugin['plugin_url'] = $plugin_data['PluginURI'];
$plugin['version'] = $plugin_data['Version'];
$plugin['description'] = $plugin_data['Description'];
$plugin['author'] = $plugin_data['Author'];
$plugin['author_url'] = $plugin_data['AuthorURI'];
$plugin['network'] = $plugin_data['Network'];
$plugin['update'] = $this->get_plugin_updates( $plugin_file );
$plugin['next_autoupdate'] = gmdate( 'Y-m-d H:i:s', wp_next_scheduled( 'wp_maybe_auto_update' ) );
$action_link = $this->get_plugin_action_links( $plugin_file );
if ( ! empty( $action_link ) ) {
$plugin['action_links'] = $action_link;
}
$plugin['plugin'] = $plugin_file;
if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$autoupdate = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR );
$plugin['autoupdate'] = $autoupdate;
$autoupdate_translation = in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true );
$plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
$plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file );
if ( is_multisite() ) {
$plugin['network_active'] = is_plugin_active_for_network( $plugin_file );
}
if ( ! empty( $this->log[ $plugin_file ] ) ) {
$plugin['log'] = $this->log[ $plugin_file ];
}
return $plugin;
}
/**
* Format the plugin for v1_2.
*
* @param string $plugin_file - the plugin file.
* @param array $plugin_data - the plugin data.
*
* @return array
*/
protected function format_plugin_v1_2( $plugin_file, $plugin_data ) {
$plugin = array();
$plugin['slug'] = Jetpack_Autoupdate::get_plugin_slug( $plugin_file );
$plugin['active'] = Jetpack::is_plugin_active( $plugin_file );
$plugin['name'] = preg_replace( '/(.+)\.php$/', '$1', $plugin_file );
$plugin['display_name'] = $plugin_data['Name'];
$plugin['plugin_url'] = $plugin_data['PluginURI'];
$plugin['version'] = $plugin_data['Version'];
$plugin['description'] = $plugin_data['Description'];
$plugin['author'] = $plugin_data['Author'];
$plugin['author_url'] = $plugin_data['AuthorURI'];
$plugin['network'] = $plugin_data['Network'];
$plugin['update'] = $this->get_plugin_updates( $plugin_file );
$action_link = $this->get_plugin_action_links( $plugin_file );
if ( ! empty( $action_link ) ) {
$plugin['action_links'] = $action_link;
}
$plugin['plugin'] = $plugin_file;
if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$autoupdate = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR );
$plugin['autoupdate'] = $autoupdate;
$autoupdate_translation = $this->plugin_has_translations_autoupdates_enabled( $plugin_file );
$plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
$plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file );
if ( is_multisite() ) {
$plugin['network_active'] = is_plugin_active_for_network( $plugin_file );
}
if ( ! empty( $this->log[ $plugin_file ] ) ) {
$plugin['log'] = $this->log[ $plugin_file ];
}
return $plugin;
}
/**
* Check if plugin has autoupdates for translations enabled.
*
* @param string $plugin_file - the plugin file.
*
* @return bool
*/
protected function plugin_has_translations_autoupdates_enabled( $plugin_file ) {
return (bool) in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true );
}
/**
* Get file mod capabilities.
*/
protected function get_file_mod_capabilities() {
$reasons_can_not_autoupdate = array();
$reasons_can_not_modify_files = array();
$has_file_system_write_access = Functions::file_system_write_access();
if ( ! $has_file_system_write_access ) {
$reasons_can_not_modify_files['has_no_file_system_write_access'] = __( 'The file permissions on this host prevent editing files.', 'jetpack' );
}
$disallow_file_mods = Constants::get_constant( 'DISALLOW_FILE_MODS' );
if ( $disallow_file_mods ) {
$reasons_can_not_modify_files['disallow_file_mods'] = __( 'File modifications are explicitly disabled by a site administrator.', 'jetpack' );
}
$automatic_updater_disabled = Constants::get_constant( 'AUTOMATIC_UPDATER_DISABLED' );
if ( $automatic_updater_disabled ) {
$reasons_can_not_autoupdate['automatic_updater_disabled'] = __( 'Any autoupdates are explicitly disabled by a site administrator.', 'jetpack' );
}
if ( is_multisite() ) {
// is it the main network ? is really is multi network
if ( Jetpack::is_multi_network() ) {
$reasons_can_not_modify_files['is_multi_network'] = __( 'Multi network install are not supported.', 'jetpack' );
}
// Is the site the main site here.
if ( ! is_main_site() ) {
$reasons_can_not_modify_files['is_sub_site'] = __( 'The site is not the main network site', 'jetpack' );
}
}
$file_mod_capabilities = array(
'modify_files' => (bool) empty( $reasons_can_not_modify_files ), // install, remove, update
'autoupdate_files' => (bool) empty( $reasons_can_not_modify_files ) && empty( $reasons_can_not_autoupdate ), // enable autoupdates
);
if ( ! empty( $reasons_can_not_modify_files ) ) {
$file_mod_capabilities['reasons_modify_files_unavailable'] = $reasons_can_not_modify_files;
}
if ( ! $file_mod_capabilities['autoupdate_files'] ) {
$file_mod_capabilities['reasons_autoupdate_unavailable'] = array_merge( $reasons_can_not_autoupdate, $reasons_can_not_modify_files );
}
return $file_mod_capabilities;
}
/**
* Get plugins.
*
* @return array
*/
protected function get_plugins() {
$plugins = array();
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
$installed_plugins = apply_filters( 'all_plugins', get_plugins() );
foreach ( $this->plugins as $plugin ) {
if ( ! isset( $installed_plugins[ $plugin ] ) ) {
continue;
}
$formatted_plugin = $this->format_plugin( $plugin, $installed_plugins[ $plugin ] );
// If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
if ( $this->accepts_site_based_authentication() ) {
$plugins[] = $formatted_plugin;
continue;
}
/*
* Do not show network-active plugins
* to folks who do not have the permission to see them.
*/
if (
/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
! apply_filters( 'show_network_active_plugins', current_user_can( 'manage_network_plugins' ) )
&& ! empty( $formatted_plugin['network_active'] )
&& true === $formatted_plugin['network_active']
) {
continue;
}
$plugins[] = $formatted_plugin;
}
$args = $this->query_args();
if ( isset( $args['offset'] ) ) {
$plugins = array_slice( $plugins, (int) $args['offset'] );
}
if ( isset( $args['limit'] ) ) {
$plugins = array_slice( $plugins, 0, (int) $args['limit'] );
}
return $plugins;
}
/**
* Validate network wide.
*
* @return bool|WP_Error
*/
protected function validate_network_wide() {
$args = $this->input();
if ( isset( $args['network_wide'] ) && $args['network_wide'] ) {
$this->network_wide = true;
}
// If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
if ( $this->accepts_site_based_authentication() ) {
return true;
}
if ( $this->network_wide && ! current_user_can( 'manage_network_plugins' ) ) {
return new WP_Error( 'unauthorized', __( 'This user is not authorized to manage plugins network wide.', 'jetpack' ), 403 );
}
return true;
}
/**
* Validate the plugin.
*
* @param string $plugin - the plugin we're validating.
*
* @return bool|WP_Error
*/
protected function validate_plugin( $plugin ) {
if ( ! isset( $plugin ) || empty( $plugin ) ) {
return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin to activate.', 'jetpack' ), 400 );
}
$error = validate_plugin( $plugin );
if ( is_wp_error( $error ) ) {
return new WP_Error( 'unknown_plugin', $error->get_error_messages(), 404 );
}
return true;
}
/**
* Validates if scheduled updates are allowed based on the current plan.
*
* @return bool|WP_Error True if scheduled updates are allowed or not provided, WP_Error otherwise.
*/
protected function validate_scheduled_update() {
$args = $this->input();
if ( isset( $args['scheduled_update'] ) && $args['scheduled_update'] ) {
if ( Current_Plan::supports( 'scheduled-updates' ) ) {
$this->scheduled_update = true;
} else {
return new WP_Error( 'unauthorized', __( 'Scheduled updates are not available on your current plan. Please upgrade to a plan that supports scheduled updates to use this feature.', 'jetpack' ), 403 );
}
}
return true;
}
/**
* Get plugin updates.
*
* @param string $plugin_file - the plugin file.
*
* @return object|null
*/
protected function get_plugin_updates( $plugin_file ) {
$plugin_updates = get_plugin_updates();
if ( isset( $plugin_updates[ $plugin_file ] ) ) {
$update = $plugin_updates[ $plugin_file ]->update;
$cleaned_update = array();
foreach ( (array) $update as $update_key => $update_value ) {
switch ( $update_key ) {
case 'id':
case 'slug':
case 'plugin':
case 'new_version':
case 'tested':
$cleaned_update[ $update_key ] = wp_kses( $update_value, array() );
break;
case 'url':
case 'package':
$cleaned_update[ $update_key ] = esc_url( $update_value );
break;
}
}
return (object) $cleaned_update;
}
return null;
}
/**
* Get plugin action links.
*
* @param string $plugin_file - the plugin file.
*
* @return array
*/
protected function get_plugin_action_links( $plugin_file ) {
return Functions::get_plugins_action_links( $plugin_file );
}
}
@@ -0,0 +1,43 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* JSON API plugins get endpoint.
*/
new Jetpack_JSON_API_Plugins_Get_Endpoint(
array(
'description' => 'Get the Plugin data.',
'method' => 'GET',
'path' => '/sites/%s/plugins/%s/',
'min_version' => '1',
'max_version' => '1.1',
'stat' => 'plugins:1',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(string) The plugin ID',
),
'allow_jetpack_site_auth' => true,
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello',
)
);
/**
* Plugins get endpoint class.
*
* GET /sites/%s/plugins/%s
*
* No v1.2 version since it is .com only
*/
class Jetpack_JSON_API_Plugins_Get_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'activate_plugins';
}
@@ -0,0 +1,126 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Plugins_Installer;
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
// POST /sites/%s/plugins/%s/install
new Jetpack_JSON_API_Plugins_Install_Endpoint(
array(
'description' => 'Install a plugin to your jetpack blog',
'group' => '__do_not_document',
'stat' => 'plugins:1:install',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/install',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(int|string) The plugin slug to install',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/akismet/install',
)
);
new Jetpack_JSON_API_Plugins_Install_Endpoint(
array(
'description' => 'Install a plugin to your jetpack blog',
'group' => '__do_not_document',
'stat' => 'plugins:1:install',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/install',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(int|string) The plugin slug to install',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/akismet/install',
)
);
/**
* Plugins install enedpoint class.
*
* POST /sites/%s/plugins/%s/install
*/
class Jetpack_JSON_API_Plugins_Install_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'install_plugins';
/**
* The action.
*
* @var string
*/
protected $action = 'install';
/**
* Installation.
*
* @return bool|WP_Error
*/
protected function install() {
$result = '';
foreach ( $this->plugins as $index => $slug ) {
$result = Plugins_Installer::install_plugin( $slug );
if ( is_wp_error( $result ) ) {
$this->log[ $slug ][] = $result->get_error_message();
if ( ! $this->bulk ) {
return $result;
}
}
}
if ( ! $result ) {
return new WP_Error( 'plugin_install_failed', __( 'Plugin install failed because the result was invalid.', 'jetpack' ) );
}
if ( is_wp_error( $result ) ) {
return $result;
}
// No errors, install worked. Now replace the slug with the actual plugin id
$this->plugins[ $index ] = Plugins_Installer::get_plugin_id_by_slug( $slug );
return true;
}
/**
* Validate the plugins.
*
* @return bool|WP_Error
*/
protected function validate_plugins() {
if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) {
return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) );
}
foreach ( $this->plugins as $slug ) {
// make sure it is not already installed
if ( Plugins_Installer::get_plugin_id_by_slug( $slug ) ) {
return new WP_Error( 'plugin_already_installed', __( 'The plugin is already installed', 'jetpack' ) );
}
}
return true;
}
}
@@ -0,0 +1,54 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new Jetpack_JSON_API_Plugins_List_Endpoint(
array(
'description' => 'Get installed Plugins on your blog',
'method' => 'GET',
'path' => '/sites/%s/plugins',
'stat' => 'plugins',
'min_version' => '1',
'max_version' => '1.1',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
),
'allow_jetpack_site_auth' => true,
'response_format' => array(
'plugins' => '(plugin) An array of plugin objects.',
),
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins',
)
);
/**
* Plugins list endpoint class.
*
* GET /sites/%s/plugins
*
* No v1.2 versions since they are .com only
*/
class Jetpack_JSON_API_Plugins_List_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'activate_plugins';
/**
* Validate the input.
*
* @param string $plugin - the plugin.
*
* @return bool
*/
public function validate_input( $plugin ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
wp_update_plugins();
$this->plugins = array_keys( get_plugins() );
return true;
}
}
@@ -0,0 +1,559 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Constants;
new Jetpack_JSON_API_Plugins_Modify_Endpoint(
array(
'description' => 'Activate/Deactivate a Plugin on your Jetpack Site, or set automatic updates',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s',
'stat' => 'plugins:1:modify',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(string) The plugin ID',
),
'allow_jetpack_site_auth' => true,
'request_format' => array(
'action' => '(string) Possible values are \'update\'',
'autoupdate' => '(bool) Whether or not to automatically update the plugin',
'active' => '(bool) Activate or deactivate the plugin',
'network_wide' => '(bool) Do action network wide (default value: false)',
'scheduled_update' => '(bool) If the update is happening as a result of a scheduled update event',
),
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'action' => 'update',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello',
)
);
new Jetpack_JSON_API_Plugins_Modify_Endpoint(
array(
'description' => 'Activate/Deactivate a list of plugins on your Jetpack Site, or set automatic updates',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins',
'stat' => 'plugins:modify',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
),
'request_format' => array(
'action' => '(string) Possible values are \'update\'',
'autoupdate' => '(bool) Whether or not to automatically update the plugin',
'active' => '(bool) Activate or deactivate the plugin',
'network_wide' => '(bool) Do action network wide (default value: false)',
'plugins' => '(array) A list of plugin ids to modify',
),
'allow_jetpack_site_auth' => true,
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => array(
'plugins' => '(array:plugin) An array of plugin objects.',
'updated' => '(array) A list of plugin ids that were updated. Only present if action is update.',
'not_updated' => '(array) A list of plugin ids that were not updated. Only present if action is update.',
'log' => '(array) Update log. Only present if action is update.',
),
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'active' => true,
'plugins' => array(
'jetpack/jetpack',
'akismet/akismet',
),
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins',
)
);
new Jetpack_JSON_API_Plugins_Modify_Endpoint(
array(
'description' => 'Update a Plugin on your Jetpack Site',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/update/',
'stat' => 'plugins:1:update',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(string) The plugin ID',
),
'allow_jetpack_site_auth' => true,
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello/update',
)
);
/**
* Plugins modify endpoint class.
*
* POST /sites/%s/plugins/%s
* POST /sites/%s/plugins
*/
class Jetpack_JSON_API_Plugins_Modify_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* The slug.
*
* @var string
*/
protected $slug = null;
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'activate_plugins';
/**
* Action.
*
* @var string
*/
protected $action = 'default_action';
/**
* Expected actions.
*
* @var array
*/
protected $expected_actions = array( 'update', 'install', 'delete', 'update_translations' );
/**
* Callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param object $object - the object.
*
* @return bool|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $object = null ) {
Jetpack_JSON_API_Endpoint::validate_input( $object );
switch ( $this->action ) {
case 'delete':
$this->needed_capabilities = 'delete_plugins';
break;
case 'update_translations':
case 'update':
$this->needed_capabilities = 'update_plugins';
break;
case 'install':
$this->needed_capabilities = 'install_plugins';
break;
}
$args = $this->input();
if ( is_array( $args ) && ( isset( $args['autoupdate'] ) || isset( $args['autoupdate_translations'] ) ) ) {
$this->needed_capabilities = 'update_plugins';
}
return parent::callback( $path, $blog_id, $object );
}
/**
* The default action.
*
* @return bool|WP_Error
*/
public function default_action() {
$args = $this->input();
if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) {
if ( $args['autoupdate'] ) {
$this->autoupdate_on();
} else {
$this->autoupdate_off();
}
}
if ( isset( $args['active'] ) && is_bool( $args['active'] ) ) {
if ( $args['active'] ) {
// We don't have to check for activate_plugins permissions since we assume that the user has those
// Since we set them via $needed_capabilities.
return $this->activate();
} elseif ( $this->current_user_can( 'deactivate_plugins' ) ) {
return $this->deactivate();
} else {
return new WP_Error( 'unauthorized_error', __( 'Plugin deactivation is not allowed', 'jetpack' ), '403' );
}
}
if ( isset( $args['autoupdate_translations'] ) && is_bool( $args['autoupdate_translations'] ) ) {
if ( $args['autoupdate_translations'] ) {
$this->autoupdate_translations_on();
} else {
$this->autoupdate_translations_off();
}
}
return true;
}
/**
* Turn on autoupdate.
*/
protected function autoupdate_on() {
$autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() );
$autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) );
update_site_option( 'auto_update_plugins', $autoupdate_plugins );
}
/**
* Turn off autoupdate.
*/
protected function autoupdate_off() {
$autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() );
$autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins );
update_site_option( 'auto_update_plugins', $autoupdate_plugins );
}
/**
* Turn autoupdate translations on.
*/
protected function autoupdate_translations_on() {
$autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
$autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) );
Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins );
}
/**
* Turn autoupdate translations off.
*/
protected function autoupdate_translations_off() {
$autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
$autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins );
Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins );
}
/**
* Activate the plugin.
*
* @return null|WP_Error null if the activation was successful.
*/
protected function activate() {
$permission_error = false;
foreach ( $this->plugins as $plugin ) {
if ( ! $this->current_user_can( 'activate_plugin', $plugin ) ) {
$this->log[ $plugin ]['error'] = __( 'Sorry, you are not allowed to activate this plugin.', 'jetpack' );
$has_errors = true;
$permission_error = true;
continue;
}
if ( ( ! $this->network_wide && Jetpack::is_plugin_active( $plugin ) ) || is_plugin_active_for_network( $plugin ) ) {
$this->log[ $plugin ]['error'] = __( 'The Plugin is already active.', 'jetpack' );
$has_errors = true;
continue;
}
if ( ! $this->network_wide && is_network_only_plugin( $plugin ) && is_multisite() ) {
$this->log[ $plugin ]['error'] = __( 'Plugin can only be Network Activated', 'jetpack' );
$has_errors = true;
continue;
}
$result = activate_plugin( $plugin, '', $this->network_wide );
if ( is_wp_error( $result ) ) {
$this->log[ $plugin ]['error'] = $result->get_error_messages();
$has_errors = true;
continue;
}
$success = Jetpack::is_plugin_active( $plugin );
if ( $success && $this->network_wide ) {
$success &= is_plugin_active_for_network( $plugin );
}
if ( ! $success ) {
$this->log[ $plugin ]['error'] = $result->get_error_messages;
$has_errors = true;
continue;
}
$this->log[ $plugin ][] = __( 'Plugin activated.', 'jetpack' );
}
if ( ! $this->bulk && isset( $has_errors ) ) {
$plugin = $this->plugins[0];
if ( $permission_error ) {
return new WP_Error( 'unauthorized_error', $this->log[ $plugin ]['error'], 403 );
}
return new WP_Error( 'activation_error', $this->log[ $plugin ]['error'] );
}
}
/**
* Check if the current user has capabilities.
*
* @param string $capability - the capability we're checking.
* @param string $plugin - the plugin we're checking.
*
* @return bool
*/
protected function current_user_can( $capability, $plugin = null ) {
// If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
if ( $this->accepts_site_based_authentication() ) {
return true;
}
if ( $plugin ) {
return current_user_can( $capability, $plugin );
}
return current_user_can( $capability );
}
/**
* Deactivate the plugin.
*
* @return null|WP_Error null if the deactivation was successful
*/
protected function deactivate() {
$permission_error = false;
foreach ( $this->plugins as $plugin ) {
if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) {
$error = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' );
$this->log[ $plugin ]['error'] = $error;
$permission_error = true;
continue;
}
if ( ! Jetpack::is_plugin_active( $plugin ) ) {
$error = __( 'The Plugin is already deactivated.', 'jetpack' );
$this->log[ $plugin ]['error'] = $error;
continue;
}
deactivate_plugins( $plugin, false, $this->network_wide );
$success = ! Jetpack::is_plugin_active( $plugin );
if ( $success && $this->network_wide ) {
$success &= ! is_plugin_active_for_network( $plugin );
}
if ( ! $success ) {
$error = __( 'There was an error deactivating your plugin', 'jetpack' );
$this->log[ $plugin ]['error'] = $error;
continue;
}
$this->log[ $plugin ][] = __( 'Plugin deactivated.', 'jetpack' );
}
if ( ! $this->bulk && isset( $error ) ) {
if ( $permission_error ) {
return new WP_Error( 'unauthorized_error', $error, 403 );
}
return new WP_Error( 'deactivation_error', $error );
}
}
/**
* Update the plugin.
*
* @return bool|WP_Error
*/
protected function update() {
$query_args = $this->query_args();
$is_automatic_update = false;
if ( isset( $query_args['autoupdate'] ) && $query_args['autoupdate'] || $this->scheduled_update ) {
$is_automatic_update = true;
}
if ( $this->scheduled_update ) {
Constants::set_constant( 'SCHEDULED_AUTOUPDATE', true );
}
wp_clean_plugins_cache( false );
ob_start();
wp_update_plugins(); // Check for Plugin updates
ob_end_clean();
$update_plugins = get_site_transient( 'update_plugins' );
if ( isset( $update_plugins->response ) ) {
$plugin_updates_needed = array_keys( $update_plugins->response );
} else {
$plugin_updates_needed = array();
}
$update_attempted = false;
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// unhook this functions that output things before we send our response header.
remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
remove_action( 'upgrader_process_complete', 'wp_version_check' );
remove_action( 'upgrader_process_complete', 'wp_update_themes' );
// Set the lock timeout to 15 minutes if it's scheduled update, otherwise default to one hour.
$lock_release_timeout = $this->scheduled_update ? 15 * MINUTE_IN_SECONDS : null;
// Early return if unable to obtain auto_updater lock.
// @see https://github.com/WordPress/wordpress-develop/blob/66469efa99e7978c8824e287834135aa9842e84f/src/wp-admin/includes/class-wp-automatic-updater.php#L453.
if ( $is_automatic_update && ! WP_Upgrader::create_lock( 'auto_updater', $lock_release_timeout ) ) {
return new WP_Error( 'update_fail', __( 'Updates are already in progress.', 'jetpack' ), 400 );
}
$result = false;
foreach ( $this->plugins as $plugin ) {
if ( ! in_array( $plugin, $plugin_updates_needed, true ) ) {
$this->log[ $plugin ][] = __( 'No update needed', 'jetpack' );
continue;
}
// Rely on WP_Automatic_Updater class to check if a plugin item should be updated if it is a Jetpack autoupdate request.
if ( $is_automatic_update && ! ( new WP_Automatic_Updater() )->should_update( 'plugin', $update_plugins->response[ $plugin ], WP_PLUGIN_DIR ) ) {
continue;
}
// Establish per plugin lock.
$plugin_slug = Jetpack_Autoupdate::get_plugin_slug( $plugin );
if ( ! WP_Upgrader::create_lock( 'jetpack_' . $plugin_slug, $lock_release_timeout ) ) {
continue;
}
/**
* Pre-upgrade action
*
* @since 3.9.3
*
* @param array $plugin Plugin data
* @param array $plugin Array of plugin objects
* @param bool $updated_attempted false for the first update, true subsequently
*/
do_action( 'jetpack_pre_plugin_upgrade', $plugin, $this->plugins, $update_attempted );
$update_attempted = true;
// Object created inside the for loop to clean the messages for each plugin
$skin = new WP_Ajax_Upgrader_Skin();
// The Automatic_Upgrader_Skin skin shouldn't output anything.
$upgrader = new Plugin_Upgrader( $skin );
$upgrader->init();
// This avoids the plugin to be deactivated.
// Using bulk upgrade puts the site into maintenance mode during the upgrades
$result = $upgrader->bulk_upgrade( array( $plugin ) );
$errors = $skin->get_errors();
$this->log[ $plugin ] = $skin->get_upgrade_messages();
// release individual plugin lock.
WP_Upgrader::release_lock( 'jetpack_' . $plugin_slug );
if ( is_wp_error( $errors ) && $errors->get_error_code() ) {
return $errors;
}
}
// release auto_udpate lock.
if ( $is_automatic_update ) {
WP_Upgrader::release_lock( 'auto_updater' );
}
if ( ! $this->bulk && ! $result && $update_attempted ) {
return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 );
}
return $this->default_action();
}
/**
* Update translations.
*
* @return bool|WP_Error
*/
public function update_translations() {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// Clear the cache.
wp_clean_plugins_cache( false );
ob_start();
wp_update_plugins(); // Check for Plugin updates
ob_end_clean();
$available_updates = get_site_transient( 'update_plugins' );
if ( ! isset( $available_updates->translations ) || empty( $available_updates->translations ) ) {
return new WP_Error( 'nothing_to_translate' );
}
$update_attempted = false;
$result = false;
foreach ( $this->plugins as $plugin ) {
$this->slug = Jetpack_Autoupdate::get_plugin_slug( $plugin );
$translation = array_filter( $available_updates->translations, array( $this, 'get_translation' ) );
if ( empty( $translation ) ) {
$this->log[ $plugin ][] = __( 'No update needed', 'jetpack' );
continue;
}
/**
* Pre-upgrade action
*
* @since 4.4.0
*
* @param array $plugin Plugin data
* @param array $plugin Array of plugin objects
* @param bool $update_attempted false for the first update, true subsequently
*/
do_action( 'jetpack_pre_plugin_upgrade_translations', $plugin, $this->plugins, $update_attempted );
$update_attempted = true;
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Language_Pack_Upgrader( $skin );
$upgrader->init();
$result = $upgrader->upgrade( (object) $translation[0] );
$this->log[ $plugin ] = $upgrader->skin->get_upgrade_messages();
}
if ( ! $this->bulk && ! $result ) {
return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 );
}
return true;
}
/**
* Test whether the translation matches `$this->slug`.
*
* @param array $translation - the translation.
*
* @return bool
*/
protected function get_translation( $translation ) {
return ( $translation['slug'] === $this->slug );
}
}
@@ -0,0 +1,220 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint(
array(
'description' => 'Activate/Deactivate a Plugin on your Jetpack Site, or set automatic updates',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s',
'stat' => 'plugins:1:modify',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(string) The plugin ID',
),
'request_format' => array(
'action' => '(string) Possible values are \'update\'',
'autoupdate' => '(bool) Whether or not to automatically update the plugin',
'active' => '(bool) Activate or deactivate the plugin',
'network_wide' => '(bool) Do action network wide (default value: false)',
),
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'action' => 'update',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/hello-dolly%20hello',
)
);
new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint(
array(
'description' => 'Activate/Deactivate a list of plugins on your Jetpack Site, or set automatic updates',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins',
'stat' => 'plugins:modify',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
),
'request_format' => array(
'action' => '(string) Possible values are \'update\'',
'autoupdate' => '(bool) Whether or not to automatically update the plugin',
'active' => '(bool) Activate or deactivate the plugin',
'network_wide' => '(bool) Do action network wide (default value: false)',
'plugins' => '(array) A list of plugin ids to modify',
),
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => array(
'plugins' => '(array:plugin_v1_2) An array of plugin objects.',
'updated' => '(array) A list of plugin ids that were updated. Only present if action is update.',
'not_updated' => '(array) A list of plugin ids that were not updated. Only present if action is update.',
'log' => '(array) Update log. Only present if action is update.',
),
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
'body' => array(
'active' => true,
'plugins' => array(
'jetpack/jetpack',
'akismet/akismet',
),
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins',
)
);
new Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint(
array(
'description' => 'Update a Plugin on your Jetpack Site',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/%s/update/',
'stat' => 'plugins:1:update',
'path_labels' => array(
'$site' => '(int|string) The site ID, The site domain',
'$plugin' => '(string) The plugin ID',
),
'query_parameters' => array(
'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/hello-dolly%20hello/update',
)
);
/**
* Plugins modify 1_2 Endpoint.
*/
class Jetpack_JSON_API_Plugins_Modify_v1_2_Endpoint extends Jetpack_JSON_API_Plugins_Modify_Endpoint { // phpcs:ignore PEAR.NamingConventions.ValidClassName.Invalid, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
/**
* Activate plugins.
*
* @return null|WP_Error null on success, WP_Error otherwise.
*/
protected function activate() {
$permission_error = false;
$has_errors = false;
foreach ( $this->plugins as $plugin ) {
// If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
if ( ! $this->accepts_site_based_authentication() ) {
if ( ! $this->current_user_can( 'activate_plugin', $plugin ) ) {
$this->log[ $plugin ]['error'] = __( 'Sorry, you are not allowed to activate this plugin.', 'jetpack' );
$has_errors = true;
$permission_error = true;
continue;
}
}
if ( ( ! $this->network_wide && Jetpack::is_plugin_active( $plugin ) ) || is_plugin_active_for_network( $plugin ) ) {
continue;
}
if ( ! $this->network_wide && is_network_only_plugin( $plugin ) && is_multisite() ) {
$this->log[ $plugin ]['error'] = __( 'Plugin can only be Network Activated', 'jetpack' );
$has_errors = true;
continue;
}
$result = activate_plugin( $plugin, '', $this->network_wide );
if ( is_wp_error( $result ) ) {
$this->log[ $plugin ]['error'] = $result->get_error_messages();
$has_errors = true;
continue;
}
$success = Jetpack::is_plugin_active( $plugin );
if ( $success && $this->network_wide ) {
$success &= is_plugin_active_for_network( $plugin );
}
if ( ! $success ) {
$this->log[ $plugin ]['error'] = $result->get_error_messages;
$has_errors = true;
continue;
}
$this->log[ $plugin ][] = __( 'Plugin activated.', 'jetpack' );
}
if ( ! $this->bulk && $has_errors ) {
$plugin = $this->plugins[0];
if ( $permission_error ) {
return new WP_Error( 'unauthorized_error', $this->log[ $plugin ]['error'], 403 );
}
return new WP_Error( 'activation_error', $this->log[ $plugin ]['error'] );
}
}
/**
* Deactivate plugins.
*
* @return null|WP_Error null on success, WP_Error otherwise.
*/
protected function deactivate() {
$permission_error = false;
foreach ( $this->plugins as $plugin ) {
// If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
if ( ! $this->accepts_site_based_authentication() ) {
if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) {
$error = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' );
$this->log[ $plugin ]['error'] = $error;
$permission_error = true;
continue;
}
}
if ( ! Jetpack::is_plugin_active( $plugin ) ) {
continue;
}
deactivate_plugins( $plugin, false, $this->network_wide );
$success = ! Jetpack::is_plugin_active( $plugin );
if ( $success && $this->network_wide ) {
$success &= ! is_plugin_active_for_network( $plugin );
}
if ( ! $success ) {
$error = __( 'There was an error deactivating your plugin', 'jetpack' );
$this->log[ $plugin ]['error'] = $error;
continue;
}
$this->log[ $plugin ][] = __( 'Plugin deactivated.', 'jetpack' );
}
if ( ! $this->bulk && isset( $error ) ) {
if ( $permission_error ) {
return new WP_Error( 'unauthorized_error', $error, 403 );
}
return new WP_Error( 'deactivation_error', $error );
}
}
}
@@ -0,0 +1,170 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
use Automattic\Jetpack\Automatic_Install_Skin;
// POST /sites/%s/plugins/new
new Jetpack_JSON_API_Plugins_New_Endpoint(
array(
'description' => 'Install a plugin to a Jetpack site by uploading a zip file',
'group' => '__do_not_document',
'stat' => 'plugins:new',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(array) Reference to an uploaded plugin package zip file.',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/new',
)
);
new Jetpack_JSON_API_Plugins_New_Endpoint(
array(
'description' => 'Install a plugin to a Jetpack site by uploading a zip file',
'group' => '__do_not_document',
'stat' => 'plugins:new',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(array) Reference to an uploaded plugin package zip file.',
),
'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/new',
)
);
/**
* Plugins new endpoint class.
*
* POST /sites/%s/plugins/new
*/
class Jetpack_JSON_API_Plugins_New_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'install_plugins';
/**
* The action.
*
* @var string
*/
protected $action = 'install';
/**
* Validate call.
*
* @param int $_blog_id - the blog ID.
* @param string $capability - the capability.
* @param bool $check_manage_active - check if manage is active.
*
* @return bool|WP_Error a WP_Error object or true if things are good.
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
$validate = parent::validate_call( $_blog_id, $capability, $check_manage_active );
if ( is_wp_error( $validate ) ) {
// Lets delete the attachment... if the user doesn't have the right permissions to do things.
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) ) {
wp_delete_attachment( $args['zip'][0]['id'], true );
}
}
return $validate;
}
/**
* No need to try to validate the plugin since we didn't pass one in.
*
* @param string $plugin - the plugin we're validating.
*/
protected function validate_input( $plugin ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$this->bulk = false;
$this->plugins = array();
}
/**
* Install the plugin.
*
* @return bool|WP_Error
*/
public function install() {
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) ) {
$plugin_attachment_id = $args['zip'][0]['id'];
$local_file = get_attached_file( $plugin_attachment_id );
if ( ! $local_file ) {
return new WP_Error( 'local-file-does-not-exist' );
}
$skin = new Automatic_Install_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$pre_install_plugin_list = get_plugins();
$result = $upgrader->install( $local_file );
// clean up.
wp_delete_attachment( $plugin_attachment_id, true );
if ( is_wp_error( $result ) ) {
return $result;
}
$after_install_plugin_list = get_plugins();
$plugin = array_values( array_diff( array_keys( $after_install_plugin_list ), array_keys( $pre_install_plugin_list ) ) );
if ( ! $result ) {
$error_code = $skin->get_main_error_code();
$message = $skin->get_main_error_message();
if ( empty( $message ) ) {
$message = __( 'An unknown error occurred during installation', 'jetpack' );
}
if ( 'download_failed' === $error_code ) {
$error_code = 'no_package';
}
return new WP_Error( $error_code, $message, 400 );
}
if ( empty( $plugin ) ) {
return new WP_Error( 'plugin_already_installed' );
}
$this->plugins = $plugin;
$this->log[ $plugin[0] ] = $upgrader->skin->get_upgrade_messages();
return true;
}
return new WP_Error( 'no_plugin_installed' );
}
}

Some files were not shown because too many files have changed in this diff Show More