This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,66 @@
<?php
/**
* Shared attachment-ownership and zip-mime validation for JSON API endpoints
* that consume an uploaded zip (plugin/theme install and replace).
*
* @package automattic/jetpack
*/
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Shared ownership check for endpoints that consume an uploaded attachment
* (plugin/theme zip uploads). Guards against an authorized caller referencing
* an attachment that wasn't part of this upload.
*/
trait Jetpack_JSON_API_Attachment_Ownership_Trait {
/**
* Confirm the attachment exists, is the attachment post type, and is owned
* by the current user when a user is identifiable. Site-auth requests
* (no current user) skip the author check.
*
* @param int $attachment_id Attachment post ID.
* @return true|WP_Error
*/
protected function validate_attachment_ownership( $attachment_id ) {
$post = get_post( $attachment_id );
if ( ! $post || 'attachment' !== $post->post_type ) {
return new WP_Error( 'invalid_attachment', __( 'The referenced upload is not a valid attachment.', 'jetpack' ), 400 );
}
$current_user_id = get_current_user_id();
if ( $current_user_id && (int) $post->post_author !== $current_user_id ) {
return new WP_Error( 'attachment_not_owned', __( 'The referenced upload does not belong to the current user.', 'jetpack' ), 403 );
}
return true;
}
/**
* Confirm the attachment is a zip package. Reject non-zip mime types and
* non-.zip extensions to prevent unrelated uploads (images, PDFs, etc.)
* from being fed to Plugin_Upgrader / Theme_Upgrader.
*
* @param int $attachment_id Attachment post ID.
* @return true|WP_Error
*/
protected function validate_attachment_is_zip( $attachment_id ) {
$mime = get_post_mime_type( $attachment_id );
if ( 'application/zip' !== $mime ) {
return new WP_Error( 'invalid_attachment_mime', __( 'Uploaded attachment is not a zip package.', 'jetpack' ), 400 );
}
$file = get_attached_file( $attachment_id );
if ( $file ) {
$extension = strtolower( (string) pathinfo( $file, PATHINFO_EXTENSION ) );
if ( 'zip' !== $extension ) {
return new WP_Error( 'invalid_attachment_extension', __( 'Uploaded attachment is not a .zip file.', 'jetpack' ), 400 );
}
}
return true;
}
}
@@ -0,0 +1,89 @@
<?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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* API endpoint /sites/%s/delete-backup-helper-script
* This API endpoint deletes a Jetpack Backup Helper Script
*
* @phan-constructor-used-for-side-effects
*/
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,94 @@
<?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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* API endpoint /sites/%s/install-backup-helper-script
* This API endpoint installs a Helper Script to assist Jetpack Backup fetch data
*
* @phan-constructor-used-for-side-effects
*/
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,57 @@
<?php
/**
* List modules v1.2 endpoint.
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Status;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* List modules v1.2 endpoint.
*
* @phan-constructor-used-for-side-effects
*/
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,45 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Check capabilities endpoint class.
*
* GET /sites/%s/me/capability
*
* @phan-constructor-used-for-side-effects
*/
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,52 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Core endpoint class.
*
* POST /sites/%s/core
* POST /sites/%s/core/update
*
* @phan-constructor-used-for-side-effects
*/
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,124 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Core modify endpoint class.
*
* POST /sites/%s/core
* POST /sites/%s/core/update
*
* @phan-constructor-used-for-side-effects
*/
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 = $args['version'] ?? false;
$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,345 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Cron endpoint class.
*
* GET /sites/%s/cron
*
* @phan-constructor-used-for-side-effects
*/
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
*
* @phan-constructor-used-for-side-effects
*/
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
*
* @phan-constructor-used-for-side-effects
*/
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
*
* @phan-constructor-used-for-side-effects
*/
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,181 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
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 = ( $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,87 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* The Get comment backup endpoint class.
*
* /sites/%s/comments/%d/backup -> $blog_id, $comment_id
*
* @phan-constructor-used-for-side-effects
*/
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,142 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get Database object backup endpoint class.
*
* /sites/%s/database-object/backup -> $blog_id
*
* @phan-constructor-used-for-side-effects
*/
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,74 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get option backup endpoint.
*
* /sites/%s/options/backup -> $blog_id
*
* @phan-constructor-used-for-side-effects
*/
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 array
*/
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,75 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get post backup endpoint class.
*
* /sites/%s/posts/%d/backup -> $blog_id, $post_id
*
* @phan-constructor-used-for-side-effects
*/
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,66 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get Term backup endpoint class.
*
* /sites/%s/terms/%d/backup -> $blog_id, $term_id
*
* @phan-constructor-used-for-side-effects
*/
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,66 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get user Backup endpoint class.
*
* /sites/%s/users/%d/backup -> $blog_id, $user_id
*
* @phan-constructor-used-for-side-effects
*/
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,84 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* JPS WooCommerce connect endpoint.
*
* @phan-constructor-used-for-side-effects
*/
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,36 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Jetpack log endpoint class.
*
* GET /sites/%s/jetpack-log
*
* @phan-constructor-used-for-side-effects
*/
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,62 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Auto update endpoint class.
*
* POST /sites/%s/maybe_auto_update
*
* @phan-constructor-used-for-side-effects
*/
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,161 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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,21 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* The Modules get endpoint.
*
* /sites/%s/jetpack/modules/%s
*
* @phan-constructor-used-for-side-effects
*/
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,34 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileNames
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Modules list endpoint.
*
* GET /sites/%s/jetpack/modules
*
* @phan-constructor-used-for-side-effects
*/
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,105 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Modules modify endpoint class.
*
* POST /sites/%s/jetpack/modules/%s/activate
* POST /sites/%s/jetpack/modules/%s
* POST /sites/%s/jetpack/modules
*
* @phan-constructor-used-for-side-effects
*/
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';
/**
* Keeps track of module logs.
*
* @var array
*/
public $log;
/**
* 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,108 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
// 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
*
* @phan-constructor-used-for-side-effects
*/
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,501 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\Sync\Functions;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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 .= '.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 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' => empty( $reasons_can_not_modify_files ), // install, remove, update
'autoupdate_files' => 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,49 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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
*
* @phan-constructor-used-for-side-effects
*/
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,134 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Plugins_Installer;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
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
*
* @phan-constructor-used-for-side-effects
*/
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
// @todo This code looks a bit suspect; why do we loop through plugins and only look at the last one?
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- we return early if there is an issue; otherwise $index and $slug are set in the foreach loop
$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,62 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
new Jetpack_JSON_API_Plugins_List_Endpoint(
array(
'description' => 'Get installed Plugins on your blog',
'method' => 'GET',
'path' => '/sites/%s/plugins',
'rest_route' => '/plugins',
'rest_min_jp_version' => '15.9',
'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
*
* @phan-constructor-used-for-side-effects
*/
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,565 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
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
*
* @phan-constructor-used-for-side-effects
*/
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,227 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
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.
*
* @phan-constructor-used-for-side-effects
*/
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,176 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Automatic_Install_Skin;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
// POST /sites/%s/plugins/new
new Jetpack_JSON_API_Plugins_New_Endpoint(
array(
'description' => 'Install a plugin to a Jetpack site by uploading a zip file',
'group' => '__do_not_document',
'stat' => 'plugins:new',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/new',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(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
*
* @phan-constructor-used-for-side-effects
*/
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' );
}
}
@@ -0,0 +1,270 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Automatic_Install_Skin;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Install-or-replace a plugin via zip upload. Passes overwrite_package=true to
* Plugin_Upgrader so an existing plugin at the same slug is replaced in place,
* mirroring wp-admin's "Replace current with uploaded" confirmation flow.
*
* POST /sites/%s/plugins/replace
*
* ## Authentication trust model
*
* Two auth modes are supported:
*
* 1. User auth — the request is mapped to a WordPress user who must hold
* `install_plugins` AND `update_plugins`. The ownership check verifies the
* referenced attachment was authored by that same user, preventing the
* endpoint from being used to touch another user's attachments.
*
* 2. Site-based auth (`allow_jetpack_site_auth => true`) — no user is
* identified; capability and ownership checks are skipped. The wpcom
* upload forwarder is expected to (a) vouch for the caller, (b) create the
* attachment as part of the same request's upload-intercept pipeline, and
* (c) pass the resulting ID through unchanged. If that contract is broken
* on the wpcom side, any trusted site credential becomes install-anything
* on the site.
*
* ## Cross-user attachment-deletion guard
*
* `Jetpack_JSON_API_Plugins_New_Endpoint::validate_call` deletes the referenced
* attachment on capability-check failure without verifying the caller owns it.
* This Replace endpoint overrides `validate_call` to run the ownership check
* first, so a low-privilege caller cannot use it to hard-delete another user's
* attachment as a side-effect of the cap check failing. The parent's behavior
* is unchanged for the legitimate case (caller's own attachment is cleaned up
* when their cap check fails).
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Plugins_Replace_Endpoint extends Jetpack_JSON_API_Plugins_New_Endpoint {
use Jetpack_JSON_API_Attachment_Ownership_Trait;
/**
* Replace is destructive, so require both install and update caps.
*
* @var array
*/
protected $needed_capabilities = array( 'install_plugins', 'update_plugins' );
/**
* Error codes we are willing to surface to the caller. Anything outside
* this list is collapsed to 'install_failed' with a generic message to
* avoid leaking filesystem paths from Plugin_Upgrader internals.
*
* Kept aligned with codes actually emitted by `WP_Upgrader` /
* `Plugin_Upgrader` — $this->strings[] message keys are NOT error codes
* and do not belong here.
*
* @var array
*/
protected static $allowed_error_codes = array(
'no_package',
'bad_request',
'files_not_writable',
'copy_dir_failed',
'remove_old_failed',
'source_read_failed',
'new_source_read_failed',
'mkdir_failed_destination',
'folder_exists',
'incompatible_archive',
'incompatible_archive_no_plugins',
'incompatible_archive_empty',
'incompatible_php_required_version',
'incompatible_wp_required_version',
'unable_to_connect_to_filesystem',
'fs_unavailable',
'fs_error',
'fs_no_plugins_dir',
'fs_no_folder',
'fs_no_root_dir',
'fs_no_content_dir',
'fs_no_temp_backup_dir',
'fs_temp_backup_mkdir',
'fs_temp_backup_move',
);
/**
* Install, replacing any existing plugin at the same slug.
*
* @return bool|WP_Error
*/
public function install() {
$args = $this->input();
if ( ! isset( $args['zip'][0]['id'] ) || ! is_scalar( $args['zip'][0]['id'] ) ) {
return new WP_Error( 'no_plugin_installed', __( 'No plugin zip file was provided.', 'jetpack' ), 400 );
}
$expected_slug = isset( $args['slug'] ) && is_scalar( $args['slug'] )
? strtolower( (string) $args['slug'] )
: '';
if ( ! preg_match( '/^[a-z0-9][a-z0-9_-]*$/', $expected_slug ) ) {
return new WP_Error( 'missing_slug', __( 'A valid plugin slug is required; the replace endpoint refuses to overwrite a plugin whose slug the caller has not declared.', 'jetpack' ), 400 );
}
$plugin_attachment_id = (int) $args['zip'][0]['id'];
// Re-checked here so direct invocations of install() (notably the test stubs)
// can't accidentally bypass the ownership guard that validate_call() runs on
// the live request path.
$ownership = $this->validate_attachment_ownership( $plugin_attachment_id );
if ( is_wp_error( $ownership ) ) {
return $ownership;
}
$zip_check = $this->validate_attachment_is_zip( $plugin_attachment_id );
if ( is_wp_error( $zip_check ) ) {
wp_delete_attachment( $plugin_attachment_id, true );
return $zip_check;
}
$local_file = get_attached_file( $plugin_attachment_id );
if ( ! $local_file ) {
wp_delete_attachment( $plugin_attachment_id, true );
return new WP_Error( 'local-file-does-not-exist', __( 'Uploaded plugin zip could not be found on disk.', 'jetpack' ), 400 );
}
$skin = new Automatic_Install_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$result = $upgrader->install(
$local_file,
array( 'overwrite_package' => true )
);
wp_delete_attachment( $plugin_attachment_id, true );
if ( is_wp_error( $result ) ) {
return $this->sanitize_upgrader_error( $result );
}
if ( ! $result ) {
$error_code = $skin->get_main_error_code();
if ( 'download_failed' === $error_code ) {
$error_code = 'no_package';
}
if ( empty( $error_code ) || ! in_array( $error_code, self::$allowed_error_codes, true ) ) {
$error_code = 'install_failed';
}
return new WP_Error( $error_code, __( 'Plugin installation failed.', 'jetpack' ), 400 );
}
$plugin_file = $upgrader->plugin_info();
if ( empty( $plugin_file ) ) {
return new WP_Error( 'plugin_replace_info_missing', __( 'Plugin was installed but its identifier could not be determined.', 'jetpack' ), 500 );
}
// `overwrite_package=true` trusts the zip's own folder name, so a zip whose
// top-level folder differs from the declared slug would clobber an unrelated
// plugin. Verify the post-install identifier matches the caller's contract.
$installed_slug = dirname( $plugin_file );
if ( '.' === $installed_slug || strtolower( $installed_slug ) !== $expected_slug ) {
return new WP_Error( 'slug_mismatch', __( 'The installed plugin does not match the declared slug.', 'jetpack' ), 400 );
}
$this->plugins = array( $plugin_file );
$this->log[ $plugin_file ] = $upgrader->skin->get_upgrade_messages();
return true;
}
/**
* See class docblock — runs the attachment-ownership check before delegating
* to the parent's validate_call(), which deletes the referenced attachment
* on capability-check failure without verifying ownership.
*
* @param int $_blog_id Blog ID.
* @param string $capability Capability.
* @param bool $check_manage_active Whether to check manage-is-active.
* @return bool|WP_Error
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) && is_scalar( $args['zip'][0]['id'] ) ) {
$ownership = $this->validate_attachment_ownership( (int) $args['zip'][0]['id'] );
if ( is_wp_error( $ownership ) ) {
return $ownership;
}
}
return parent::validate_call( $_blog_id, $capability, $check_manage_active );
}
/**
* Collapse unknown error codes and strip potentially path-leaking messages
* from WP_Error instances returned by Plugin_Upgrader.
*
* @param WP_Error $error Raw upgrader error.
* @return WP_Error
*/
protected function sanitize_upgrader_error( WP_Error $error ) {
$code = $error->get_error_code();
if ( empty( $code ) || ! in_array( $code, self::$allowed_error_codes, true ) ) {
return new WP_Error( 'install_failed', __( 'Plugin installation failed.', 'jetpack' ), 400 );
}
return new WP_Error( $code, __( 'Plugin installation failed.', 'jetpack' ), 400 );
}
}
// POST /sites/%s/plugins/replace
new Jetpack_JSON_API_Plugins_Replace_Endpoint(
array(
'description' => 'Install or replace a plugin on a Jetpack site by uploading a zip file. If a plugin with the same slug is already installed, its destination folder is replaced in place, mirroring wp-admin\'s "Replace current with uploaded" upload flow.',
'group' => '__do_not_document',
'stat' => 'plugins:replace',
'min_version' => '1',
'max_version' => '1.1',
'method' => 'POST',
'path' => '/sites/%s/plugins/replace',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(array) Reference to an uploaded plugin package zip file.',
'slug' => '(string) The plugin slug the uploaded zip must resolve to. Required; the endpoint rejects zips whose top-level folder does not match.',
),
'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/replace',
)
);
new Jetpack_JSON_API_Plugins_Replace_Endpoint(
array(
'description' => 'Install or replace a plugin on a Jetpack site by uploading a zip file. If a plugin with the same slug is already installed, its destination folder is replaced in place, mirroring wp-admin\'s "Replace current with uploaded" upload flow.',
'group' => '__do_not_document',
'stat' => 'plugins:replace',
'min_version' => '1.2',
'method' => 'POST',
'path' => '/sites/%s/plugins/replace',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(array) Reference to an uploaded plugin package zip file.',
'slug' => '(string) The plugin slug the uploaded zip must resolve to. Required; the endpoint rejects zips whose top-level folder does not match.',
),
'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/replace',
)
);
@@ -0,0 +1,666 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Sync\Actions;
use Automattic\Jetpack\Sync\Health;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Queue;
use Automattic\Jetpack\Sync\Queue_Buffer;
use Automattic\Jetpack\Sync\Replicastore;
use Automattic\Jetpack\Sync\Sender;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
/**
* Sync endpoint class.
*
* POST /sites/%s/sync
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* This endpoint allows authentication both via a blog and a user token.
* If a user token is used, that user should have `manage_options` capability.
*
* @var array|string
*/
protected $needed_capabilities = 'manage_options';
/**
* Validate the call.
*
* @param int $_blog_id - the blog ID.
* @param string $capability - the capability.
* @param bool $check_manage_active - unused.
*
* @return bool|WP_Error
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return parent::validate_call( $_blog_id, $capability, false );
}
/**
* The result.
*
* @return array
*/
protected function result() {
$args = $this->input();
$modules = null;
// convert list of modules in comma-delimited format into an array
// of "$modulename => true"
if ( isset( $args['modules'] ) && ! empty( $args['modules'] ) ) {
$modules = array_map( '__return_true', array_flip( array_map( 'trim', explode( ',', $args['modules'] ) ) ) );
}
foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
if ( 'users' === $module_name && isset( $args[ $module_name ] ) && 'initial' === $args[ $module_name ] ) {
$modules['users'] = 'initial';
} elseif ( isset( $args[ $module_name ] ) ) {
$ids = explode( ',', $args[ $module_name ] );
if ( is_countable( $ids ) && count( $ids ) > 0 ) {
$modules[ $module_name ] = $ids;
}
}
}
if ( empty( $modules ) ) {
$modules = null;
}
return array( 'scheduled' => Actions::do_full_sync( $modules, 'jetpack_json_api_sync_endpoint' ) );
}
/**
* Validate the queue.
*
* @param array $query - the query.
*
* @return string|WP_Error
*/
protected function validate_queue( $query ) {
if ( ! isset( $query ) ) {
return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
}
if ( ! in_array( $query, array( 'sync', 'full_sync', 'immediate' ), true ) ) {
return new WP_Error( 'invalid_queue', 'Queue name should be sync, full_sync or immediate', 400 );
}
return $query;
}
}
/**
* Sync status endpoint class.
*
* GET /sites/%s/sync/status
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Status_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Callback for the endpoint.
*
* @return array
*/
protected function result() {
$args = $this->query_args();
$fields = $args['fields'] ?? array();
return Actions::get_sync_status( $fields );
}
}
/**
* Sync Check Endpoint class.
* GET /sites/%s/data-check
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Check_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Callback for the endpoint.
*
* @return array
*/
protected function result() {
Actions::mark_sync_read_only();
$store = new Replicastore();
return $store->checksum_all();
}
}
/**
* Sync histogram endpoint.
* GET /sites/%s/data-histogram
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Histogram_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Callback for endpoint.
*
* @return array
*/
protected function result() {
$args = $this->query_args();
if ( isset( $args['columns'] ) ) {
$columns = array_map( 'trim', explode( ',', $args['columns'] ) );
} else {
$columns = null; // go with defaults
}
$store = new Replicastore();
if ( ! isset( $args['strip_non_ascii'] ) ) {
$args['strip_non_ascii'] = true;
}
/**
* Hack: nullify the values of `start_id` and `end_id` if we're only requesting ranges.
*
* The endpoint doesn't support nullable values :(
*/
if ( true === $args['only_range_edges'] ) {
if ( 0 === $args['start_id'] ) {
$args['start_id'] = null;
}
if ( 0 === $args['end_id'] ) {
$args['end_id'] = null;
}
}
$histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $columns, $args['strip_non_ascii'], $args['shared_salt'], $args['only_range_edges'], $args['detailed_drilldown'] );
// Hack to disable Sync during this call, so we can resolve faster.
Actions::mark_sync_read_only();
return array(
'histogram' => $histogram,
'type' => $store->get_checksum_type(),
);
}
}
/**
* POST /sites/%s/sync/health
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Modify_Health_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Callback for sync/health endpoint.
*
* @return array|WP_Error result of request.
*/
protected function result() {
$args = $this->input();
switch ( $args['status'] ) {
case Health::STATUS_IN_SYNC:
case Health::STATUS_OUT_OF_SYNC:
Health::update_status( $args['status'] );
break;
default:
return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' );
}
// re-fetch so we see what's really being stored.
return array(
'success' => Health::get_status(),
);
}
}
/**
* POST /sites/%s/sync/settings
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Modify_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* The endpoint callback.
*
* @return array
*/
protected function result() {
$args = $this->input();
$sync_settings = Settings::get_settings();
foreach ( $args as $key => $value ) {
if ( $value !== false ) {
if ( is_numeric( $value ) ) {
$value = (int) $value;
}
// special case for sending empty arrays - a string with value 'empty'
if ( $value === 'empty' ) {
$value = array();
}
$sync_settings[ $key ] = $value;
}
}
Settings::update_settings( $sync_settings );
// re-fetch so we see what's really being stored
return Settings::get_settings();
}
}
/**
* GET /sites/%s/sync/settings
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Get_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array
*/
protected function result() {
return Settings::get_settings();
}
}
/**
* GET /sites/%s/sync/object
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Object extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->query_args();
$module_name = $args['module_name'];
$sync_module = Modules::get_module( $module_name );
if ( ! $sync_module ) {
return new WP_Error( 'invalid_module', 'You specified an invalid sync module' );
}
$object_type = $args['object_type'];
$object_ids = $args['object_ids'];
$codec = Sender::get_instance()->get_codec();
Actions::mark_sync_read_only();
Settings::set_is_syncing( true );
$objects = $codec->encode( $sync_module->get_objects_by_id( $object_type, $object_ids ) );
Settings::set_is_syncing( false );
return array(
'objects' => $objects,
'codec' => $codec->name(),
);
}
}
/**
* Sync Now endpoint class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Now_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array
*/
protected function result() {
$args = $this->input();
$queue_name = $this->validate_queue( $args['queue'] );
if ( is_wp_error( $queue_name ) ) {
return $queue_name;
}
$sender = Sender::get_instance();
$response = $sender->do_sync_for_queue( new Queue( $args['queue'] ) );
return array(
'response' => $response,
);
}
}
/**
* Sync checkout endpoint.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Checkout_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->input();
$queue_name = $this->validate_queue( $args['queue'] );
if ( is_wp_error( $queue_name ) ) {
return $queue_name;
}
if ( $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) {
return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and less then 100', 400 );
}
$number_of_items = absint( $args['number_of_items'] );
if ( 'immediate' === $queue_name ) {
return $this->immediate_full_sync_pull( $number_of_items );
}
return $this->queue_pull( $queue_name, $number_of_items, $args );
}
/**
* Create a queue.
*
* @param string $queue_name - the queue name.
* @param int $number_of_items - the number of items.
* @param array $args - the arguments.
*
* @return array|WP_Error
*/
public function queue_pull( $queue_name, $number_of_items, $args ) {
$queue = new Queue( $queue_name );
if ( 0 === $queue->size() ) {
return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
}
$sender = Sender::get_instance();
// try to give ourselves as much time as possible.
set_time_limit( 0 );
if ( $args['pop'] ) {
$buffer = new Queue_Buffer( 'pop', $queue->pop( $number_of_items ) );
} else {
// let's delete the checkin state.
if ( $args['force'] ) {
$queue->unlock();
}
$buffer = $this->get_buffer( $queue, $number_of_items );
}
// Check that the $buffer is not checkout out already.
if ( is_wp_error( $buffer ) ) {
return new WP_Error( 'buffer_open', "We couldn't get the buffer it is currently checked out", 400 );
}
if ( ! is_object( $buffer ) ) {
return new WP_Error( 'buffer_non-object', 'Buffer is not an object', 400 );
}
Settings::set_is_syncing( true );
list( $items_to_send, $skipped_items_ids ) = $sender->get_items_to_send( $buffer, $args['encode'] );
Settings::set_is_syncing( false );
return array(
'buffer_id' => $buffer->id,
'items' => $items_to_send,
'skipped_items' => $skipped_items_ids,
'codec' => $args['encode'] ? $sender->get_codec()->name() : null,
'sent_timestamp' => time(),
);
}
/**
* The items.
*
* @var array
*/
public $items = array();
/**
* Send the data listener.
*/
public function jetpack_sync_send_data_listener() {
foreach ( func_get_args()[0] as $key => $item ) {
$this->items[ $key ] = $item;
}
}
/**
* Check out a buffer of full sync actions.
*
* @param null $number_of_items Number of Actions to check-out.
*
* @return array Sync Actions to be returned to requestor
*/
public function immediate_full_sync_pull( $number_of_items = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// try to give ourselves as much time as possible.
set_time_limit( 0 );
$original_send_data_cb = array( 'Automattic\Jetpack\Sync\Actions', 'send_data' );
$temp_send_data_cb = array( $this, 'jetpack_sync_send_data_listener' );
Sender::get_instance()->set_enqueue_wait_time( 0 );
remove_filter( 'jetpack_sync_send_data', $original_send_data_cb );
add_filter( 'jetpack_sync_send_data', $temp_send_data_cb, 10, 6 );
Sender::get_instance()->do_full_sync();
remove_filter( 'jetpack_sync_send_data', $temp_send_data_cb );
add_filter( 'jetpack_sync_send_data', $original_send_data_cb, 10, 6 );
return array(
'items' => $this->items,
'codec' => Sender::get_instance()->get_codec()->name(),
'sent_timestamp' => time(),
'status' => Actions::get_sync_status(),
);
}
/**
* Get the queue buffer.
*
* @param object $queue - the queue.
* @param int $number_of_items - the number of items.
*
* @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
*/
protected function get_buffer( $queue, $number_of_items ) {
$start = time();
$max_duration = 5; // this will try to get the buffer
$buffer = $queue->checkout( $number_of_items );
$duration = time() - $start;
while ( is_wp_error( $buffer ) && $duration < $max_duration ) {
sleep( 2 );
$duration = time() - $start;
$buffer = $queue->checkout( $number_of_items );
}
if ( $buffer === false ) {
return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
}
return $buffer;
}
}
/**
* Close endpoint class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Close_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
$request_body = $this->input();
$queue_name = $this->validate_queue( $request_body['queue'] );
if ( is_wp_error( $queue_name ) ) {
return $queue_name;
}
if ( ! isset( $request_body['buffer_id'] ) ) {
return new WP_Error( 'missing_buffer_id', 'Please provide a buffer id', 400 );
}
if ( ! isset( $request_body['item_ids'] ) || ! is_array( $request_body['item_ids'] ) ) {
return new WP_Error( 'missing_item_ids', 'Please provide a list of item ids in the item_ids argument', 400 );
}
$request_body['buffer_id'] = preg_replace( '/[^A-Za-z0-9]/', '', $request_body['buffer_id'] );
$request_body['item_ids'] = array_filter( array_map( array( 'Jetpack_JSON_API_Sync_Close_Endpoint', 'sanitize_item_ids' ), $request_body['item_ids'] ) );
$queue = new Queue( $queue_name );
$items = $queue->peek_by_id( $request_body['item_ids'] );
// Update Full Sync Status if queue is "full_sync".
if ( 'full_sync' === $queue_name ) {
$full_sync_module = Modules::get_module( 'full-sync' );
'@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $full_sync_module';
$full_sync_module->update_sent_progress_action( $items );
}
$buffer = new Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] );
$response = $queue->close( $buffer, $request_body['item_ids'] );
// Perform another checkout?
if ( isset( $request_body['continue'] ) && $request_body['continue'] ) {
if ( in_array( $queue_name, array( 'full_sync', 'immediate' ), true ) ) {
// Send Full Sync Actions.
Sender::get_instance()->do_full_sync();
} elseif ( $queue->has_any_items() ) {
// Send Incremental Sync Actions.
Sender::get_instance()->do_sync();
}
}
if ( is_wp_error( $response ) ) {
return $response;
}
return array(
'success' => $response,
'status' => Actions::get_sync_status(),
);
}
/**
* Sanitize item IDs.
*
* @param string $item - the item we're sanitizing.
*
* @return string|null
*/
protected static function sanitize_item_ids( $item ) {
// lets not delete any options that don't start with jpsq_sync-
if ( ! is_string( $item ) || ! str_starts_with( $item, 'jpsq_' ) ) {
return null;
}
// Limit to A-Z,a-z,0-9,_,-,.
return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item );
}
}
/**
* Unlock endpoint class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Unlock_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->input();
if ( ! isset( $args['queue'] ) ) {
return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
}
if ( ! in_array( $args['queue'], array( 'sync', 'full_sync' ), true ) ) {
return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
}
$queue = new Queue( $args['queue'] );
// False means that there was no lock to delete.
$response = $queue->unlock();
return array(
'success' => $response,
);
}
}
/**
* Object ID range class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Sync_Object_Id_Range extends Jetpack_JSON_API_Sync_Endpoint {
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
$args = $this->query_args();
$module_name = $args['sync_module'];
$batch_size = $args['batch_size'];
if ( ! $this->is_valid_sync_module( $module_name ) ) {
return new WP_Error( 'invalid_module', 'This sync module cannot be used to calculate a range.', 400 );
}
$module = Modules::get_module( $module_name );
return array(
'ranges' => $module->get_min_max_object_ids_for_batches( $batch_size ),
);
}
/**
* Check if sync module is valid.
*
* @param string $module_name - the module name.
*
* @return bool
*/
protected function is_valid_sync_module( $module_name ) {
return in_array(
$module_name,
array(
'comments',
'posts',
'terms',
'term_relationships',
'users',
),
true
);
}
}
@@ -0,0 +1,90 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* GET /sites/%s/themes/mine => current theme
* POST /sites/%s/themes/mine => switch theme
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Active_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Endpoint callback.
*
* @param string $path - the path.
* @param int $blog_id - the blog ID.
* @param object $object - The unused $object parameter is for making the method signature compatible with its parent class method.
*
* @return array|bool|WP_Error
*/
public function callback( $path = '', $blog_id = 0, $object = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$error = $this->validate_call( $blog_id, 'switch_themes', true );
if ( is_wp_error( $error ) ) {
return $error;
}
if ( 'POST' === $this->api->method ) {
return $this->switch_theme();
} else {
return $this->get_current_theme();
}
}
/**
* Switch the theme.
*
* @return array|WP_Error
*/
protected function switch_theme() {
$args = $this->input();
if ( ! isset( $args['theme'] ) || empty( $args['theme'] ) ) {
return new WP_Error( 'missing_theme', __( 'You are required to specify a theme to switch to.', 'jetpack' ), 400 );
}
$theme_slug = $args['theme'];
if ( ! $theme_slug ) {
return new WP_Error( 'theme_not_found', __( 'Theme is empty.', 'jetpack' ), 404 );
}
/**
* Trigger action before the switch theme happens.
*
* @module json-api
*
* @since 11.1
*
* @param string $theme_slug Directory name for the theme.
* @param mixed $args POST body data, including info about the theme we must switch to.
*/
do_action( 'jetpack_pre_switch_theme', $theme_slug, $args );
$theme = wp_get_theme( $theme_slug );
if ( ! $theme->exists() ) {
return new WP_Error( 'theme_not_found', __( 'The specified theme was not found.', 'jetpack' ), 404 );
}
if ( ! $theme->is_allowed() ) {
return new WP_Error( 'theme_not_found', __( 'You are not allowed to switch to this theme', 'jetpack' ), 403 );
}
switch_theme( $theme_slug );
return $this->get_current_theme();
}
/**
* Get the current theme.
*
* @return array
*/
protected function get_current_theme() {
return $this->format_theme( wp_get_theme() );
}
}
@@ -0,0 +1,87 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Themes delete endpoint class.
* POST /sites/%s/plugins/%s/delete
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Delete_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'delete_themes';
/**
* The action.
*
* @var string
*/
protected $action = 'delete';
/**
* Delete the theme.
*
* @return bool|WP_Error
*/
protected function delete() {
foreach ( $this->themes as $theme ) {
// Don't delete an active child theme
if ( is_child_theme() && $theme === get_stylesheet() ) {
$error = 'You cannot delete a theme while it is active on the main site.';
$this->log[ $theme ]['error'] = $error;
continue;
}
if ( $theme === get_template() ) {
$error = 'You cannot delete a theme while it is active on the main site.';
$this->log[ $theme ]['error'] = $error;
continue;
}
/**
* Filters whether to use an alternative process for deleting a WordPress.com theme.
* The alternative process can be executed during the filter.
*
* The filter can also return an instance of WP_Error; in which case the endpoint response will
* contain this error.
*
* @module json-api
*
* @since 4.4.2
*
* @param bool $use_alternative_delete_method Whether to use the alternative method of deleting
* a WPCom theme.
* @param string $theme_slug Theme name (slug). If it is a WPCom theme,
* it should be suffixed with `-wpcom`.
*/
$result = apply_filters( 'jetpack_wpcom_theme_delete', false, $theme );
if ( ! $result ) {
$result = delete_theme( $theme );
}
if ( is_wp_error( $result ) ) {
$error = $result->get_error_messages();
$this->log[ $theme ]['error'] = $error;
} else {
$this->log[ $theme ][] = 'Theme deleted';
}
}
if ( ! $this->bulk && isset( $error ) ) {
return new WP_Error( 'delete_theme_error', $error, 400 );
}
return true;
}
}
@@ -0,0 +1,221 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Base class for working with themes, has useful helper functions.
*/
abstract class Jetpack_JSON_API_Themes_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* The themes.
*
* @var array
*/
protected $themes = array();
/**
* If we're working in bulk.
*
* @var boolean
*/
protected $bulk = true;
/**
* The log.
*
* @var array
*/
protected $log;
/**
* The current theme ID.
*
* @var int
*/
protected $current_theme_id;
/**
* The response format.
*
* @var array
*/
public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
'id' => '(string) The theme\'s ID.',
'screenshot' => '(string) A theme screenshot URL',
'name' => '(string) The name of the theme.',
'theme_uri' => '(string) The URI of the theme\'s webpage.',
'description' => '(string) A description of the theme.',
'author' => '(string) The author of the theme.',
'author_uri' => '(string) The website of the theme author.',
'tags' => '(array) Tags indicating styles and features of the theme.',
'log' => '(array) An array of log strings',
'update' => '(array|null) An object containing information about the available update if there is an update available, null otherwise.',
'autoupdate' => '(bool) Whether the theme is automatically updated',
'autoupdate_translation' => '(bool) Whether the theme is automatically updating translations',
);
/**
* The result.
*/
protected function result() {
$themes = $this->get_themes();
if ( ! $this->bulk && ! empty( $themes ) ) {
return array_pop( $themes );
}
return array( 'themes' => $themes );
}
/**
* Walks through either the submitted theme or list of themes and creates the global array
*
* @param string $theme - the theme URL.
*
* @return bool|WP_Error
*/
protected function validate_input( $theme ) {
$args = $this->input();
// lets set what themes were requested, and validate them
if ( ! isset( $theme ) || empty( $theme ) ) {
if ( ! $args['themes'] || empty( $args['themes'] ) ) {
return new WP_Error( 'missing_theme', __( 'You are required to specify a theme to update.', 'jetpack' ), 400 );
}
if ( is_array( $args['themes'] ) ) {
$this->themes = $args['themes'];
} else {
$this->themes[] = $args['themes'];
}
} else {
$this->themes[] = urldecode( $theme );
$this->bulk = false;
}
$error = $this->validate_themes();
if ( is_wp_error( $error ) ) {
return $error;
}
return parent::validate_input( $theme );
}
/**
* Walks through submitted themes to make sure they are valid
*
* @return bool|WP_Error
*/
protected function validate_themes() {
foreach ( $this->themes as $theme ) {
$error = wp_get_theme( $theme )->errors();
if ( is_wp_error( $error ) ) {
return new WP_Error( 'unknown_theme', $error->get_error_messages(), 404 );
}
}
return true;
}
/**
* Format a theme for the public API
*
* @param object $theme WP_Theme object.
* @return array Named array of theme info used by the API
*/
protected function format_theme( $theme ) {
if ( ! ( $theme instanceof WP_Theme ) ) {
$theme = wp_get_theme( $theme );
}
$fields = array(
'name' => 'Name',
'theme_uri' => 'ThemeURI',
'description' => 'Description',
'author' => 'Author',
'author_uri' => 'AuthorURI',
'tags' => 'Tags',
'version' => 'Version',
);
$id = $theme->get_stylesheet();
$formatted_theme = array(
'id' => $id,
'screenshot' => Image_CDN_Core::cdn_url( $theme->get_screenshot(), array(), 'network_path' ),
'active' => $id === $this->current_theme_id,
);
foreach ( $fields as $key => $field ) {
$formatted_theme[ $key ] = $theme->get( $field );
}
$update_themes = get_site_transient( 'update_themes' );
$formatted_theme['update'] = $update_themes->response[ $id ] ?? null;
$autoupdate = in_array( $id, Jetpack_Options::get_option( 'autoupdate_themes', array() ), true );
$formatted_theme['autoupdate'] = $autoupdate;
$autoupdate_translation = in_array( $id, Jetpack_Options::get_option( 'autoupdate_themes_translations', array() ), true );
$formatted_theme['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
if ( isset( $this->log[ $id ] ) ) {
$formatted_theme['log'] = $this->log[ $id ];
}
/**
* Filter the array of theme information that will be returned per theme by the Jetpack theme APIs.
*
* @module json-api
*
* @since 4.7.0
*
* @param array $formatted_theme The theme info array.
*/
return apply_filters( 'jetpack_format_theme_details', $formatted_theme );
}
/**
* Checks the query_args our collection endpoint was passed to ensure that it's in the proper bounds.
*
* @return bool|WP_Error a WP_Error object if the args are out of bounds, true if things are good.
*/
protected function check_query_args() {
$args = $this->query_args();
if ( $args['offset'] < 0 ) {
return new WP_Error( 'invalid_offset', __( 'Offset must be greater than or equal to 0.', 'jetpack' ), 400 );
}
if ( $args['limit'] < 0 ) {
return new WP_Error( 'invalid_limit', __( 'Limit must be greater than or equal to 0.', 'jetpack' ), 400 );
}
return true;
}
/**
* Format a list of themes for public display, using the supplied offset and limit args
*
* @uses WPCOM_JSON_API_Endpoint::query_args()
* @return array Public API theme objects
*/
protected function get_themes() {
// ditch keys
$themes = array_values( $this->themes );
// do offset & limit - we've already returned a 400 error if they're bad numbers
$args = $this->query_args();
if ( isset( $args['offset'] ) ) {
$themes = array_slice( $themes, (int) $args['offset'] );
}
if ( isset( $args['limit'] ) ) {
$themes = array_slice( $themes, 0, (int) $args['limit'] );
}
$this->current_theme_id = wp_get_theme()->get_stylesheet();
return array_map( array( $this, 'format_theme' ), $themes );
}
}
@@ -0,0 +1,21 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Themes get endpoint class.
*
* GET /sites/%s/themes/%s
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Get_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'switch_themes';
}
@@ -0,0 +1,226 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Automatic_Install_Skin;
use Automattic\Jetpack\Connection\Client;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Themes install endpoint class.
*
* POST /sites/%s/themes/%s/install
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Install_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'install_themes';
/**
* The action.
*
* @var string
*/
protected $action = 'install';
/**
* Download links.
*
* @var array
*/
protected $download_links = array();
/**
* Install the theme.
*
* @return bool|WP_Error
*/
protected function install() {
foreach ( $this->themes as $theme ) {
/**
* Filters whether to use an alternative process for installing a WordPress.com theme.
* The alternative process can be executed during the filter.
*
* The filter can also return an instance of WP_Error; in which case the endpoint response will
* contain this error.
*
* @module json-api
*
* @since 4.4.2
*
* @param bool $use_alternative_install_method Whether to use the alternative method of installing
* a WPCom theme.
* @param string $theme_slug Theme name (slug). If it is a WPCom theme,
* it should be suffixed with `-wpcom`.
*/
$result = apply_filters( 'jetpack_wpcom_theme_install', false, $theme );
$skin = null;
$upgrader = null;
$link = null;
// If the alternative install method was not used, use the standard method.
if ( ! $result ) {
$skin = new Automatic_Install_Skin();
$upgrader = new Theme_Upgrader( $skin );
$link = $this->download_links[ $theme ];
$result = $upgrader->install( $link );
}
if ( file_exists( $link ) ) {
// Delete if link was tmp local file
wp_delete_file( $link );
}
if ( ! $this->bulk && is_wp_error( $result ) ) {
return $result;
}
if ( ! $result ) {
$error = __( 'An unknown error occurred during installation', 'jetpack' );
$this->log[ $theme ]['error'] = $error;
} elseif ( ! self::is_installed_theme( $theme ) ) {
$error = __( 'There was an error installing your theme', 'jetpack' );
$this->log[ $theme ]['error'] = $error;
} elseif ( $upgrader ) {
$this->log[ $theme ][] = $upgrader->skin->get_upgrade_messages();
}
}
if ( ! $this->bulk && isset( $error ) ) {
return new WP_Error( 'install_error', $error, 400 );
}
return true;
}
/**
* Validate the themes.
*
* @return bool|WP_Error
*/
protected function validate_themes() {
if ( empty( $this->themes ) || ! is_array( $this->themes ) ) {
return new WP_Error( 'missing_themes', __( 'No themes found.', 'jetpack' ) );
}
foreach ( $this->themes as $theme ) {
if ( self::is_installed_theme( $theme ) ) {
return new WP_Error( 'theme_already_installed', __( 'The theme is already installed', 'jetpack' ) );
}
/**
* Filters whether to skip the standard method of downloading and validating a WordPress.com
* theme. An alternative method of WPCom theme download and validation can be
* executed during the filter.
*
* The filter can also return an instance of WP_Error; in which case the endpoint response will
* contain this error.
*
* @module json-api
*
* @since 4.4.2
*
* @param bool $skip_download_filter_result Whether to skip the standard method of downloading
* and validating a WPCom theme.
* @param string $theme_slug Theme name (slug). If it is a WPCom theme,
* it should be suffixed with `-wpcom`.
*/
$skip_download_filter_result = apply_filters( 'jetpack_wpcom_theme_skip_download', false, $theme );
if ( is_wp_error( $skip_download_filter_result ) ) {
return $skip_download_filter_result;
} elseif ( $skip_download_filter_result ) {
continue;
}
if ( wp_endswith( $theme, '-wpcom' ) ) {
$file = self::download_wpcom_theme_to_file( $theme );
if ( is_wp_error( $file ) ) {
return $file;
}
$this->download_links[ $theme ] = $file;
continue;
}
$params = (object) array( 'slug' => $theme );
$url = 'https://api.wordpress.org/themes/info/1.0/'; // @todo Switch to https://api.wordpress.org/themes/info/1.1/, which uses JSON rather than PHP serialization.
$args = array(
'body' => array(
'action' => 'theme_information',
'request' => serialize( $params ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
),
);
$response = wp_remote_post( $url, $args );
$theme_data = unserialize( $response['body'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
if ( is_wp_error( $theme_data ) ) {
return $theme_data;
}
if ( ! is_object( $theme_data ) && ! isset( $theme_data->download_link ) ) {
return new WP_Error( 'theme_not_found', __( 'This theme does not exist', 'jetpack' ), 404 );
}
$this->download_links[ $theme ] = $theme_data->download_link;
}
return true;
}
/**
* Check if the theme is installed.
*
* @param string $theme - the theme we're checking.
*
* @return bool
*/
protected static function is_installed_theme( $theme ) {
$wp_theme = wp_get_theme( $theme );
return $wp_theme->exists();
}
/**
* Download the wpcom theme.
*
* @param string $theme - the theme to download.
*
* @return string|WP_Error
*/
protected static function download_wpcom_theme_to_file( $theme ) {
$file = wp_tempnam( 'theme' );
if ( ! $file ) {
return new WP_Error( 'problem_creating_theme_file', __( 'Problem creating file for theme download', 'jetpack' ) );
}
$url = "themes/download/$theme.zip";
$args = array(
'stream' => true,
'filename' => $file,
);
$result = Client::wpcom_json_api_request_as_blog( $url, '1.1', $args );
$response = $result['response'];
if ( $response['code'] !== 200 ) {
wp_delete_file( $file );
return new WP_Error( 'problem_fetching_theme', __( 'Problem downloading theme', 'jetpack' ) );
}
return $file;
}
}
@@ -0,0 +1,33 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Theme list endpoint class.
*
* GET /sites/%s/themes
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_List_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'switch_themes';
/**
* Validate the input.
*
* @param string $theme - the theme we're validating (unused, for keeping in sync with parent class).
*
* @return bool
*/
public function validate_input( $theme ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$this->themes = wp_get_themes( array( 'allowed' => true ) );
return true;
}
}
@@ -0,0 +1,184 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Themes modify endpoint class.
* POST /sites/%s/themes/%s
* POST /sites/%s/themes
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Modify_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'update_themes';
/**
* The action.
*
* @var string
*/
protected $action = 'default_action';
/**
* Expected actions.
*
* @var array
*/
protected $expected_actions = array( 'update', 'update_translations' );
/**
* The default action.
*
* @return bool
*/
public function default_action() {
$args = $this->input();
if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) {
if ( $args['autoupdate'] ) {
$this->autoupdate_on();
} else {
$this->autoupdate_off();
}
}
if ( isset( $args['autoupdate_translations'] ) && is_bool( $args['autoupdate_translations'] ) ) {
if ( $args['autoupdate_translations'] ) {
$this->autoupdate_translations_on();
} else {
$this->autoupdate_translations_off();
}
}
return true;
}
/**
* Turn autoupdate on.
*/
public function autoupdate_on() {
$autoupdate_themes = Jetpack_Options::get_option( 'autoupdate_themes', array() );
$autoupdate_themes = array_unique( array_merge( $autoupdate_themes, $this->themes ) );
Jetpack_Options::update_option( 'autoupdate_themes', $autoupdate_themes );
}
/**
* Turn autoupdate off.
*/
public function autoupdate_off() {
$autoupdate_themes = Jetpack_Options::get_option( 'autoupdate_themes', array() );
$autoupdate_themes = array_diff( $autoupdate_themes, $this->themes );
Jetpack_Options::update_option( 'autoupdate_themes', $autoupdate_themes );
}
/**
* Autoupdate translations on.
*/
public function autoupdate_translations_on() {
$autoupdate_themes_translations = Jetpack_Options::get_option( 'autoupdate_themes_translations', array() );
$autoupdate_themes_translations = array_unique( array_merge( $autoupdate_themes_translations, $this->themes ) );
Jetpack_Options::update_option( 'autoupdate_themes_translations', $autoupdate_themes_translations );
}
/**
* Autoupdate translations off.
*/
public function autoupdate_translations_off() {
$autoupdate_themes_translations = Jetpack_Options::get_option( 'autoupdate_themes_translations', array() );
$autoupdate_themes_translations = array_diff( $autoupdate_themes_translations, $this->themes );
Jetpack_Options::update_option( 'autoupdate_themes_translations', $autoupdate_themes_translations );
}
/**
* Update the theme.
*
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public function update() {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// Clear the cache.
wp_update_themes();
$result = null;
foreach ( $this->themes as $theme ) {
/**
* Pre-upgrade action
*
* @since 3.9.3
*
* @param object $theme WP_Theme object
* @param array $themes Array of theme objects
*/
do_action( 'jetpack_pre_theme_upgrade', $theme, $this->themes );
// Objects created inside the for loop to clean the messages for each theme
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Theme_Upgrader( $skin );
$upgrader->init();
$result = $upgrader->upgrade( $theme );
$this->log[ $theme ][] = $upgrader->skin->get_upgrade_messages();
}
if ( ! $this->bulk && ! $result ) {
return new WP_Error( 'update_fail', __( 'There was an error updating your theme', 'jetpack' ), 400 );
}
return true;
}
/**
* Update translations.
*
* @return bool|WP_Error
*/
public function update_translations() {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// Clear the cache.
wp_update_themes();
$available_themes_updates = get_site_transient( 'update_themes' );
if ( ! isset( $available_themes_updates->translations ) || empty( $available_themes_updates->translations ) ) {
return new WP_Error( 'nothing_to_translate' );
}
$result = null;
foreach ( $available_themes_updates->translations as $translation ) {
$theme = $translation['slug'];
if ( ! in_array( $translation['slug'], $this->themes, true ) ) {
$this->log[ $theme ][] = __( 'No update needed', 'jetpack' );
continue;
}
/**
* Pre-upgrade action
*
* @since 4.4.0
*
* @param object $theme WP_Theme object
* @param array $themes Array of theme objects
*/
do_action( 'jetpack_pre_theme_upgrade_translations', $theme, $this->themes );
// Objects created inside the for loop to clean the messages for each theme
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Language_Pack_Upgrader( $skin );
$upgrader->init();
$result = $upgrader->upgrade( (object) $translation );
$this->log[ $theme ] = $upgrader->skin->get_upgrade_messages();
}
if ( ! $this->bulk && ! $result ) {
return new WP_Error( 'update_fail', __( 'There was an error updating your theme', 'jetpack' ), 400 );
}
return true;
}
}
@@ -0,0 +1,130 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Automatic_Install_Skin;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Themes new endpoint class.
*
* /sites/%s/themes/%s/install
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_New_Endpoint extends Jetpack_JSON_API_Themes_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'install_themes';
/**
* Action.
*
* @var string
*/
protected $action = 'install';
/**
* Download links.
*
* @var array
*/
protected $download_links = array();
/**
* Validate the call.
*
* @param int $_blog_id - the blod ID.
* @param string $capability - the capability we're checking.
* @param bool $check_manage_active - if managing capabilities is active.
*
* @return bool|WP_Error
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
$validate = parent::validate_call( $_blog_id, $capability, $check_manage_active );
if ( is_wp_error( $validate ) ) {
// Lets delete the attachment... if the user doesn't have the right permissions to do things.
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) ) {
wp_delete_attachment( $args['zip'][0]['id'], true );
}
}
return $validate;
}
/**
* Validate the input.
*
* @param string $theme - the theme.
*/
protected function validate_input( $theme ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$this->bulk = false;
$this->themes = array();
}
/**
* Install the theme.
*
* @return bool
*/
public function install() {
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) ) {
$attachment_id = $args['zip'][0]['id'];
$local_file = get_attached_file( $attachment_id );
if ( ! $local_file ) {
return new WP_Error( 'local-file-does-not-exist' );
}
$skin = new Automatic_Install_Skin();
$upgrader = new Theme_Upgrader( $skin );
$pre_install_list = wp_get_themes();
$result = $upgrader->install( $local_file );
// clean up.
wp_delete_attachment( $attachment_id, true );
if ( is_wp_error( $result ) ) {
return $result;
}
$after_install_list = wp_get_themes();
$plugin = array_values( array_diff( array_keys( $after_install_list ), array_keys( $pre_install_list ) ) );
if ( ! $result ) {
$error_code = $skin->get_main_error_code();
$message = $skin->get_main_error_message();
if ( empty( $message ) ) {
$message = __( 'An unknown error occurred during installation', 'jetpack' );
}
if ( 'download_failed' === $error_code ) {
$error_code = 'no_package';
}
return new WP_Error( $error_code, $message, 400 );
}
if ( empty( $plugin ) ) {
return new WP_Error( 'theme_already_installed' );
}
$this->themes = $plugin;
$this->log[ $plugin[0] ] = $upgrader->skin->get_upgrade_messages();
return true;
}
return new WP_Error( 'no_theme_installed' );
}
}
@@ -0,0 +1,244 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Automatic_Install_Skin;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Install-or-replace a theme via zip upload. Passes overwrite_package=true to
* Theme_Upgrader so an existing theme at the same slug is replaced in place,
* mirroring wp-admin's "Replace current with uploaded" confirmation flow.
*
* POST /sites/%s/themes/replace
*
* ## Authentication trust model
*
* Two auth modes are supported:
*
* 1. User auth — the request is mapped to a WordPress user who must hold
* `install_themes` AND `update_themes`. The ownership check verifies the
* referenced attachment was authored by that same user, preventing the
* endpoint from being used to touch another user's attachments.
*
* 2. Site-based auth (`allow_jetpack_site_auth => true`) — no user is
* identified; capability and ownership checks are skipped. The wpcom
* upload forwarder is expected to (a) vouch for the caller, (b) create the
* attachment as part of the same request's upload-intercept pipeline, and
* (c) pass the resulting ID through unchanged. If that contract is broken
* on the wpcom side, any trusted site credential becomes install-anything
* on the site.
*
* ## Cross-user attachment-deletion guard
*
* `Jetpack_JSON_API_Themes_New_Endpoint::validate_call` deletes the referenced
* attachment on capability-check failure without verifying the caller owns it.
* This Replace endpoint overrides `validate_call` to run the ownership check
* first, so a low-privilege caller cannot use it to hard-delete another user's
* attachment as a side-effect of the cap check failing. The parent's behavior
* is unchanged for the legitimate case (caller's own attachment is cleaned up
* when their cap check fails).
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Themes_Replace_Endpoint extends Jetpack_JSON_API_Themes_New_Endpoint {
use Jetpack_JSON_API_Attachment_Ownership_Trait;
/**
* Replace is destructive, so require both install and update caps.
*
* @var array
*/
protected $needed_capabilities = array( 'install_themes', 'update_themes' );
/**
* Error codes we are willing to surface to the caller. Anything outside
* this list is collapsed to 'install_failed' with a generic message to
* avoid leaking filesystem paths from Theme_Upgrader internals.
*
* Kept aligned with codes actually emitted by `WP_Upgrader` /
* `Theme_Upgrader` — $this->strings[] message keys are NOT error codes
* and do not belong here.
*
* @var array
*/
protected static $allowed_error_codes = array(
'no_package',
'bad_request',
'files_not_writable',
'copy_dir_failed',
'remove_old_failed',
'source_read_failed',
'new_source_read_failed',
'mkdir_failed_destination',
'folder_exists',
'incompatible_archive',
'incompatible_archive_empty',
'incompatible_archive_theme_no_style',
'incompatible_archive_theme_no_name',
'incompatible_archive_theme_no_index',
'incompatible_php_required_version',
'incompatible_wp_required_version',
'unable_to_connect_to_filesystem',
'fs_unavailable',
'fs_error',
'fs_no_themes_dir',
'fs_no_folder',
'fs_no_root_dir',
'fs_no_content_dir',
'fs_no_temp_backup_dir',
'fs_temp_backup_mkdir',
'fs_temp_backup_move',
);
/**
* Install, replacing any existing theme at the same slug.
*
* @return bool|WP_Error
*/
public function install() {
$args = $this->input();
if ( ! isset( $args['zip'][0]['id'] ) || ! is_scalar( $args['zip'][0]['id'] ) ) {
return new WP_Error( 'no_theme_installed', __( 'No theme zip file was provided.', 'jetpack' ), 400 );
}
$expected_slug = isset( $args['slug'] ) && is_scalar( $args['slug'] )
? strtolower( (string) $args['slug'] )
: '';
if ( ! preg_match( '/^[a-z0-9][a-z0-9_-]*$/', $expected_slug ) ) {
return new WP_Error( 'missing_slug', __( 'A valid theme slug is required; the replace endpoint refuses to overwrite a theme whose slug the caller has not declared.', 'jetpack' ), 400 );
}
$attachment_id = (int) $args['zip'][0]['id'];
// Re-checked here so direct invocations of install() (notably the test stubs)
// can't accidentally bypass the ownership guard that validate_call() runs on
// the live request path.
$ownership = $this->validate_attachment_ownership( $attachment_id );
if ( is_wp_error( $ownership ) ) {
return $ownership;
}
$zip_check = $this->validate_attachment_is_zip( $attachment_id );
if ( is_wp_error( $zip_check ) ) {
wp_delete_attachment( $attachment_id, true );
return $zip_check;
}
$local_file = get_attached_file( $attachment_id );
if ( ! $local_file ) {
wp_delete_attachment( $attachment_id, true );
return new WP_Error( 'local-file-does-not-exist', __( 'Uploaded theme zip could not be found on disk.', 'jetpack' ), 400 );
}
$skin = new Automatic_Install_Skin();
$upgrader = new Theme_Upgrader( $skin );
$result = $upgrader->install(
$local_file,
array( 'overwrite_package' => true )
);
wp_delete_attachment( $attachment_id, true );
if ( is_wp_error( $result ) ) {
return $this->sanitize_upgrader_error( $result );
}
if ( ! $result ) {
$error_code = $skin->get_main_error_code();
if ( 'download_failed' === $error_code ) {
$error_code = 'no_package';
}
if ( empty( $error_code ) || ! in_array( $error_code, self::$allowed_error_codes, true ) ) {
$error_code = 'install_failed';
}
return new WP_Error( $error_code, __( 'Theme installation failed.', 'jetpack' ), 400 );
}
$theme_info = $upgrader->theme_info();
$theme_slug = $theme_info ? $theme_info->get_stylesheet() : '';
if ( empty( $theme_slug ) ) {
return new WP_Error( 'theme_replace_info_missing', __( 'Theme was installed but its identifier could not be determined.', 'jetpack' ), 500 );
}
// `overwrite_package=true` trusts the zip's own folder name, so a zip whose
// top-level folder differs from the declared slug would clobber an unrelated
// theme. Verify the post-install identifier matches the caller's contract.
if ( strtolower( $theme_slug ) !== $expected_slug ) {
return new WP_Error( 'slug_mismatch', __( 'The installed theme does not match the declared slug.', 'jetpack' ), 400 );
}
$this->themes = array( $theme_slug );
$this->log[ $theme_slug ] = $upgrader->skin->get_upgrade_messages();
return true;
}
/**
* See class docblock — runs the attachment-ownership check before delegating
* to the parent's validate_call(), which deletes the referenced attachment
* on capability-check failure without verifying ownership.
*
* @param int $_blog_id Blog ID.
* @param string $capability Capability.
* @param bool $check_manage_active Whether to check manage-is-active.
* @return bool|WP_Error
*/
protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
$args = $this->input();
if ( isset( $args['zip'][0]['id'] ) && is_scalar( $args['zip'][0]['id'] ) ) {
$ownership = $this->validate_attachment_ownership( (int) $args['zip'][0]['id'] );
if ( is_wp_error( $ownership ) ) {
return $ownership;
}
}
return parent::validate_call( $_blog_id, $capability, $check_manage_active );
}
/**
* Collapse unknown error codes and strip potentially path-leaking messages
* from WP_Error instances returned by Theme_Upgrader.
*
* @param WP_Error $error Raw upgrader error.
* @return WP_Error
*/
protected function sanitize_upgrader_error( WP_Error $error ) {
$code = $error->get_error_code();
if ( empty( $code ) || ! in_array( $code, self::$allowed_error_codes, true ) ) {
return new WP_Error( 'install_failed', __( 'Theme installation failed.', 'jetpack' ), 400 );
}
return new WP_Error( $code, __( 'Theme installation failed.', 'jetpack' ), 400 );
}
}
// POST /sites/%s/themes/replace
new Jetpack_JSON_API_Themes_Replace_Endpoint(
array(
'description' => 'Install or replace a theme on a Jetpack site by uploading a zip file. If a theme with the same slug is already installed, its destination folder is replaced in place, mirroring wp-admin\'s "Replace current with uploaded" upload flow.',
'group' => '__do_not_document',
'stat' => 'themes:replace',
'method' => 'POST',
'path' => '/sites/%s/themes/replace',
'path_labels' => array(
'$site' => '(int|string) Site ID or domain',
),
'request_format' => array(
'zip' => '(array) Reference to an uploaded theme package zip file.',
'slug' => '(string) The theme slug the uploaded zip must resolve to. Required; the endpoint rejects zips whose top-level folder does not match.',
),
'response_format' => Jetpack_JSON_API_Themes_Endpoint::$_response_format,
'allow_jetpack_site_auth' => true,
'example_request_data' => array(
'headers' => array(
'authorization' => 'Bearer YOUR_API_TOKEN',
),
),
'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/themes/replace',
)
);
@@ -0,0 +1,51 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Translations endpoint class.
*
* GET /sites/%s/translations
* POST /sites/%s/translations
* POST /sites/%s/translations/update
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Translations_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var array
*/
protected $needed_capabilities = array( 'update_core', 'update_plugins', 'update_themes' );
/**
* The log.
*
* @var array
*/
protected $log;
/**
* If we're successful.
*
* @var bool
*/
protected $success;
/**
* API Endpoint.
*
* @return array
*/
public function result() {
return array(
'translations' => wp_get_translation_updates(),
'autoupdate' => Jetpack_Options::get_option( 'autoupdate_translations', false ),
'log' => $this->log,
'success' => $this->success,
);
}
}
@@ -0,0 +1,64 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Translations modify endpoint class.
* POST /sites/%s/translation
* POST /sites/%s/translations/update
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Translations_Modify_Endpoint extends Jetpack_JSON_API_Translations_Endpoint {
/**
* The action.
*
* @var string
*/
protected $action = 'default_action';
/**
* The new version.
*
* @var string
*/
protected $new_version;
/**
* The log.
*
* @var array
*/
protected $log;
/**
* Run the default action.
*
* @return true
*/
public function default_action() {
$args = $this->input();
if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) {
Jetpack_Options::update_option( 'autoupdate_translations', $args['autoupdate'] );
}
return true;
}
/**
* Update the translations.
*/
protected function update() {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$upgrader = new Language_Pack_Upgrader( new Automatic_Upgrader_Skin() );
$result = $upgrader->bulk_upgrade();
$this->log = $upgrader->skin->get_upgrade_messages();
$this->success = ( ! is_wp_error( $result ) ) ? (bool) $result : false;
}
}
@@ -0,0 +1,54 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Updates status class.
*
* GET /sites/%s/updates
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_Updates_Status extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'manage_options';
/**
* Endpoint callback.
*
* @return array|WP_Error
*/
protected function result() {
wp_update_themes();
wp_update_plugins();
$update_data = wp_get_update_data();
if ( ! isset( $update_data['counts'] ) ) {
return new WP_Error( 'get_update_data_error', __( 'There was an error while getting the update data for this site.', 'jetpack' ), 500 );
}
$result = $update_data['counts'];
include ABSPATH . WPINC . '/version.php'; // $wp_version;
// @phan-suppress-next-line PhanImpossibleCondition -- $wp_version is defined in the included version.php file above
$result['wp_version'] = $wp_version ?? null; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
if ( ! empty( $result['wordpress'] ) ) {
$cur = get_preferred_from_update_core();
if ( isset( $cur->response ) && $cur->response === 'upgrade' ) {
$result['wp_update_version'] = $cur->current;
}
}
$result['jp_version'] = JETPACK__VERSION;
return $result;
}
}
@@ -0,0 +1,70 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Tokens;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* User connect endpoint class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_User_Connect_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'create_users';
/**
* The user ID.
*
* @var int
*/
private $user_id;
/**
* The user token.
*
* @var string
*/
private $user_token;
/**
* The endpoint callback.
*
* @return array
*/
public function result() {
( new Tokens() )->update_user_token( $this->user_id, sprintf( '%s.%d', $this->user_token, $this->user_id ), false );
return array( 'success' => ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $this->user_id ) );
}
/**
* Validate input.
*
* @param int $user_id - the User ID.
*
* @return bool|WP_Error
*/
public function validate_input( $user_id ) {
$input = $this->input();
if ( ! isset( $user_id ) ) {
return new WP_Error( 'input_error', __( 'user_id is required', 'jetpack' ) );
}
$this->user_id = $user_id;
if ( ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $this->user_id ) ) {
return new WP_Error( 'user_already_connected', __( 'The user is already connected', 'jetpack' ) );
}
if ( ! isset( $input['user_token'] ) ) {
return new WP_Error( 'input_error', __( 'user_token is required', 'jetpack' ) );
}
$this->user_token = sanitize_text_field( $input['user_token'] );
return parent::validate_input( $user_id );
}
}
@@ -0,0 +1,116 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Connection\Utils;
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* User create endpoint class.
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_JSON_API_User_Create_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* Needed capabilities.
*
* @var string
*/
protected $needed_capabilities = 'create_users';
/**
* User data.
*
* @var array
*/
private $user_data;
/**
* Endpoint callback.
*
* @return object|false
*/
public function result() {
return $this->create_or_get_user();
}
/**
* Validate the input.
*
* @param object $object - the object.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) {
$this->user_data = $this->input();
if ( empty( $this->user_data ) ) {
return new WP_Error( 'input_error', __( 'user_data is required', 'jetpack' ) );
}
if ( ! isset( $this->user_data['email'] ) ) {
return new WP_Error( 'input_error', __( 'user email is required', 'jetpack' ) );
}
if ( ! isset( $this->user_data['login'] ) ) {
return new WP_Error( 'input_error', __( 'user login is required', 'jetpack' ) );
}
return parent::validate_input( $object );
}
/**
* Create or get the user.
*
* @return object|false
*/
public function create_or_get_user() {
// Check for an existing user
$user = get_user_by( 'email', $this->user_data['email'] );
$roles = (array) $this->user_data['roles'];
$role = array_pop( $roles );
$query_args = $this->query_args();
if ( isset( $query_args['invite_accepted'] ) && $query_args['invite_accepted'] ) {
Constants::set_constant( 'JETPACK_INVITE_ACCEPTED', true );
}
if ( ! $user ) {
// We modify the input here to mimick the same call structure of the update user endpoint.
$this->user_data = (object) $this->user_data;
$this->user_data->role = $role;
$this->user_data->url = $this->user_data->URL ?? '';
$this->user_data->display_name = $this->user_data->name;
$this->user_data->description = '';
$user = Utils::generate_user( $this->user_data );
}
if ( is_multisite() ) {
add_user_to_blog( get_current_blog_id(), $user->ID, $role );
}
if ( ! $user ) {
return false;
}
return $this->get_user( $user->ID );
}
/**
* Get the user.
*
* @param int $user_id - the user ID.
*
* @return object|WP_Error
*/
public function get_user( $user_id ) {
$the_user = $this->get_author( $user_id, true );
if ( $the_user && ! is_wp_error( $the_user ) ) {
$userdata = get_userdata( $user_id );
$the_user->roles = ! is_wp_error( $userdata ) ? $userdata->roles : array();
}
return $the_user;
}
}
@@ -0,0 +1,79 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Sync\Defaults;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Get option endpoint.
*
* @phan-constructor-used-for-side-effects
*/
class WPCOM_JSON_API_Get_Option_Endpoint extends Jetpack_JSON_API_Endpoint {
/**
* This endpoint allows authentication both via a blog and a user token.
* If a user token is used, that user should have `manage_options` capability.
*
* @var array|string
*/
protected $needed_capabilities = 'manage_options';
/**
* Options name.
*
* @var string
*/
public $option_name;
/**
* Site option.
*
* @var string
*/
public $site_option;
/**
* Endpoint callback.
*
* @return array
*/
public function result() {
if ( $this->site_option ) {
return array( 'option_value' => get_site_option( $this->option_name ) );
}
return array( 'option_value' => get_option( $this->option_name ) );
}
/**
* Validate the input.
*
* @param object $object - unused, for parent class compatability.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$query_args = $this->query_args();
$this->option_name = $query_args['option_name'] ?? false;
if ( ! $this->option_name ) {
return new WP_Error( 'option_name_not_set', __( 'You must specify an option_name', 'jetpack' ) );
}
$this->site_option = $query_args['site_option'] ?? false;
/**
* Filter the list of options that are manageable via the JSON API.
*
* @module json-api
*
* @since 3.8.2
*
* @param array The default list of site options.
* @param bool Is the option a site option.
*/
if ( ! in_array( $this->option_name, apply_filters( 'jetpack_options_whitelist', Defaults::$default_options_whitelist, $this->site_option ), true ) ) {
return new WP_Error( 'option_name_not_in_whitelist', __( 'You must specify a whitelisted option_name', 'jetpack' ) );
}
return true;
}
}
@@ -0,0 +1,57 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Update option endpoint.
*
* @phan-constructor-used-for-side-effects
*/
class WPCOM_JSON_API_Update_Option_Endpoint extends WPCOM_JSON_API_Get_Option_Endpoint {
/**
* The option value.
*
* @var string
*/
public $option_value;
/**
* Endpoint callback.
*
* @return array
*/
public function result() {
if ( $this->site_option ) {
update_site_option( $this->option_name, $this->option_value );
} else {
update_option( $this->option_name, $this->option_value );
}
return parent::result();
}
/**
* Validate the input.
*
* @param object $object - the object we're validating.
*
* @return bool|WP_Error
*/
public function validate_input( $object ) {
$input = $this->input();
$query_args = $this->query_args();
if ( ! isset( $input['option_value'] ) || is_array( $input['option_value'] ) ) {
return new WP_Error( 'option_value_not_set', __( 'You must specify an option_value', 'jetpack' ) );
}
if ( $query_args['is_array'] ) {
// When converted back from JSON, the value is an object.
// Cast it to an array for options that expect arrays.
$this->option_value = (array) $input['option_value'];
} else {
$this->option_value = $input['option_value'];
}
return parent::validate_input( $object );
}
}
File diff suppressed because it is too large Load Diff