initial
This commit is contained in:
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
+89
@@ -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;
|
||||
}
|
||||
}
|
||||
+94
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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 );
|
||||
}
|
||||
}
|
||||
+45
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
+345
@@ -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 );
|
||||
}
|
||||
}
|
||||
+181
@@ -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;
|
||||
}
|
||||
}
|
||||
+87
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+142
@@ -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;
|
||||
}
|
||||
}
|
||||
+74
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
+75
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+66
@@ -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 ),
|
||||
);
|
||||
}
|
||||
}
|
||||
+66
@@ -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 ),
|
||||
);
|
||||
}
|
||||
}
|
||||
+84
@@ -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 );
|
||||
}
|
||||
}
|
||||
+36
@@ -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 ),
|
||||
);
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
+161
@@ -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 );
|
||||
}
|
||||
}
|
||||
+21
@@ -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';
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+105
@@ -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;
|
||||
}
|
||||
}
|
||||
+108
@@ -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;
|
||||
}
|
||||
}
|
||||
+501
@@ -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 );
|
||||
}
|
||||
}
|
||||
+49
@@ -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';
|
||||
}
|
||||
+134
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
+565
@@ -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 );
|
||||
}
|
||||
}
|
||||
+227
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
+176
@@ -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' );
|
||||
}
|
||||
}
|
||||
+270
@@ -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',
|
||||
)
|
||||
);
|
||||
+666
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+90
@@ -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() );
|
||||
}
|
||||
}
|
||||
+87
@@ -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;
|
||||
}
|
||||
}
|
||||
+221
@@ -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 );
|
||||
}
|
||||
}
|
||||
+21
@@ -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';
|
||||
}
|
||||
+226
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+184
@@ -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;
|
||||
}
|
||||
}
|
||||
+130
@@ -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' );
|
||||
}
|
||||
}
|
||||
+244
@@ -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',
|
||||
)
|
||||
);
|
||||
+51
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+64
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+70
@@ -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 );
|
||||
}
|
||||
}
|
||||
+116
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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
Reference in New Issue
Block a user