angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-plugins-controller.php (28178B)


      1 <?php
      2 /**
      3  * REST API: WP_REST_Plugins_Controller class
      4  *
      5  * @package WordPress
      6  * @subpackage REST_API
      7  * @since 5.5.0
      8  */
      9 
     10 /**
     11  * Core class to access plugins via the REST API.
     12  *
     13  * @since 5.5.0
     14  *
     15  * @see WP_REST_Controller
     16  */
     17 class WP_REST_Plugins_Controller extends WP_REST_Controller {
     18 
     19 	const PATTERN = '[^.\/]+(?:\/[^.\/]+)?';
     20 
     21 	/**
     22 	 * Plugins controller constructor.
     23 	 *
     24 	 * @since 5.5.0
     25 	 */
     26 	public function __construct() {
     27 		$this->namespace = 'wp/v2';
     28 		$this->rest_base = 'plugins';
     29 	}
     30 
     31 	/**
     32 	 * Registers the routes for the plugins controller.
     33 	 *
     34 	 * @since 5.5.0
     35 	 */
     36 	public function register_routes() {
     37 		register_rest_route(
     38 			$this->namespace,
     39 			'/' . $this->rest_base,
     40 			array(
     41 				array(
     42 					'methods'             => WP_REST_Server::READABLE,
     43 					'callback'            => array( $this, 'get_items' ),
     44 					'permission_callback' => array( $this, 'get_items_permissions_check' ),
     45 					'args'                => $this->get_collection_params(),
     46 				),
     47 				array(
     48 					'methods'             => WP_REST_Server::CREATABLE,
     49 					'callback'            => array( $this, 'create_item' ),
     50 					'permission_callback' => array( $this, 'create_item_permissions_check' ),
     51 					'args'                => array(
     52 						'slug'   => array(
     53 							'type'        => 'string',
     54 							'required'    => true,
     55 							'description' => __( 'WordPress.org plugin directory slug.' ),
     56 							'pattern'     => '[\w\-]+',
     57 						),
     58 						'status' => array(
     59 							'description' => __( 'The plugin activation status.' ),
     60 							'type'        => 'string',
     61 							'enum'        => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
     62 							'default'     => 'inactive',
     63 						),
     64 					),
     65 				),
     66 				'schema' => array( $this, 'get_public_item_schema' ),
     67 			)
     68 		);
     69 
     70 		register_rest_route(
     71 			$this->namespace,
     72 			'/' . $this->rest_base . '/(?P<plugin>' . self::PATTERN . ')',
     73 			array(
     74 				array(
     75 					'methods'             => WP_REST_Server::READABLE,
     76 					'callback'            => array( $this, 'get_item' ),
     77 					'permission_callback' => array( $this, 'get_item_permissions_check' ),
     78 				),
     79 				array(
     80 					'methods'             => WP_REST_Server::EDITABLE,
     81 					'callback'            => array( $this, 'update_item' ),
     82 					'permission_callback' => array( $this, 'update_item_permissions_check' ),
     83 					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     84 				),
     85 				array(
     86 					'methods'             => WP_REST_Server::DELETABLE,
     87 					'callback'            => array( $this, 'delete_item' ),
     88 					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     89 				),
     90 				'args'   => array(
     91 					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     92 					'plugin'  => array(
     93 						'type'              => 'string',
     94 						'pattern'           => self::PATTERN,
     95 						'validate_callback' => array( $this, 'validate_plugin_param' ),
     96 						'sanitize_callback' => array( $this, 'sanitize_plugin_param' ),
     97 					),
     98 				),
     99 				'schema' => array( $this, 'get_public_item_schema' ),
    100 			)
    101 		);
    102 	}
    103 
    104 	/**
    105 	 * Checks if a given request has access to get plugins.
    106 	 *
    107 	 * @since 5.5.0
    108 	 *
    109 	 * @param WP_REST_Request $request Full details about the request.
    110 	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
    111 	 */
    112 	public function get_items_permissions_check( $request ) {
    113 		if ( ! current_user_can( 'activate_plugins' ) ) {
    114 			return new WP_Error(
    115 				'rest_cannot_view_plugins',
    116 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
    117 				array( 'status' => rest_authorization_required_code() )
    118 			);
    119 		}
    120 
    121 		return true;
    122 	}
    123 
    124 	/**
    125 	 * Retrieves a collection of plugins.
    126 	 *
    127 	 * @since 5.5.0
    128 	 *
    129 	 * @param WP_REST_Request $request Full details about the request.
    130 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    131 	 */
    132 	public function get_items( $request ) {
    133 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    134 
    135 		$plugins = array();
    136 
    137 		foreach ( get_plugins() as $file => $data ) {
    138 			if ( is_wp_error( $this->check_read_permission( $file ) ) ) {
    139 				continue;
    140 			}
    141 
    142 			$data['_file'] = $file;
    143 
    144 			if ( ! $this->does_plugin_match_request( $request, $data ) ) {
    145 				continue;
    146 			}
    147 
    148 			$plugins[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $data, $request ) );
    149 		}
    150 
    151 		return new WP_REST_Response( $plugins );
    152 	}
    153 
    154 	/**
    155 	 * Checks if a given request has access to get a specific plugin.
    156 	 *
    157 	 * @since 5.5.0
    158 	 *
    159 	 * @param WP_REST_Request $request Full details about the request.
    160 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
    161 	 */
    162 	public function get_item_permissions_check( $request ) {
    163 		if ( ! current_user_can( 'activate_plugins' ) ) {
    164 			return new WP_Error(
    165 				'rest_cannot_view_plugin',
    166 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
    167 				array( 'status' => rest_authorization_required_code() )
    168 			);
    169 		}
    170 
    171 		$can_read = $this->check_read_permission( $request['plugin'] );
    172 
    173 		if ( is_wp_error( $can_read ) ) {
    174 			return $can_read;
    175 		}
    176 
    177 		return true;
    178 	}
    179 
    180 	/**
    181 	 * Retrieves one plugin from the site.
    182 	 *
    183 	 * @since 5.5.0
    184 	 *
    185 	 * @param WP_REST_Request $request Full details about the request.
    186 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    187 	 */
    188 	public function get_item( $request ) {
    189 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    190 
    191 		$data = $this->get_plugin_data( $request['plugin'] );
    192 
    193 		if ( is_wp_error( $data ) ) {
    194 			return $data;
    195 		}
    196 
    197 		return $this->prepare_item_for_response( $data, $request );
    198 	}
    199 
    200 	/**
    201 	 * Checks if the given plugin can be viewed by the current user.
    202 	 *
    203 	 * On multisite, this hides non-active network only plugins if the user does not have permission
    204 	 * to manage network plugins.
    205 	 *
    206 	 * @since 5.5.0
    207 	 *
    208 	 * @param string $plugin The plugin file to check.
    209 	 * @return true|WP_Error True if can read, a WP_Error instance otherwise.
    210 	 */
    211 	protected function check_read_permission( $plugin ) {
    212 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    213 
    214 		if ( ! $this->is_plugin_installed( $plugin ) ) {
    215 			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
    216 		}
    217 
    218 		if ( ! is_multisite() ) {
    219 			return true;
    220 		}
    221 
    222 		if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) {
    223 			return true;
    224 		}
    225 
    226 		return new WP_Error(
    227 			'rest_cannot_view_plugin',
    228 			__( 'Sorry, you are not allowed to manage this plugin.' ),
    229 			array( 'status' => rest_authorization_required_code() )
    230 		);
    231 	}
    232 
    233 	/**
    234 	 * Checks if a given request has access to upload plugins.
    235 	 *
    236 	 * @since 5.5.0
    237 	 *
    238 	 * @param WP_REST_Request $request Full details about the request.
    239 	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
    240 	 */
    241 	public function create_item_permissions_check( $request ) {
    242 		if ( ! current_user_can( 'install_plugins' ) ) {
    243 			return new WP_Error(
    244 				'rest_cannot_install_plugin',
    245 				__( 'Sorry, you are not allowed to install plugins on this site.' ),
    246 				array( 'status' => rest_authorization_required_code() )
    247 			);
    248 		}
    249 
    250 		if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) {
    251 			return new WP_Error(
    252 				'rest_cannot_activate_plugin',
    253 				__( 'Sorry, you are not allowed to activate plugins.' ),
    254 				array(
    255 					'status' => rest_authorization_required_code(),
    256 				)
    257 			);
    258 		}
    259 
    260 		return true;
    261 	}
    262 
    263 	/**
    264 	 * Uploads a plugin and optionally activates it.
    265 	 *
    266 	 * @since 5.5.0
    267 	 *
    268 	 * @param WP_REST_Request $request Full details about the request.
    269 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    270 	 */
    271 	public function create_item( $request ) {
    272 		require_once ABSPATH . 'wp-admin/includes/file.php';
    273 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    274 		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
    275 		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
    276 
    277 		$slug = $request['slug'];
    278 
    279 		// Verify filesystem is accessible first.
    280 		$filesystem_available = $this->is_filesystem_available();
    281 		if ( is_wp_error( $filesystem_available ) ) {
    282 			return $filesystem_available;
    283 		}
    284 
    285 		$api = plugins_api(
    286 			'plugin_information',
    287 			array(
    288 				'slug'   => $slug,
    289 				'fields' => array(
    290 					'sections'       => false,
    291 					'language_packs' => true,
    292 				),
    293 			)
    294 		);
    295 
    296 		if ( is_wp_error( $api ) ) {
    297 			if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) {
    298 				$api->add_data( array( 'status' => 404 ) );
    299 			} else {
    300 				$api->add_data( array( 'status' => 500 ) );
    301 			}
    302 
    303 			return $api;
    304 		}
    305 
    306 		$skin     = new WP_Ajax_Upgrader_Skin();
    307 		$upgrader = new Plugin_Upgrader( $skin );
    308 
    309 		$result = $upgrader->install( $api->download_link );
    310 
    311 		if ( is_wp_error( $result ) ) {
    312 			$result->add_data( array( 'status' => 500 ) );
    313 
    314 			return $result;
    315 		}
    316 
    317 		// This should be the same as $result above.
    318 		if ( is_wp_error( $skin->result ) ) {
    319 			$skin->result->add_data( array( 'status' => 500 ) );
    320 
    321 			return $skin->result;
    322 		}
    323 
    324 		if ( $skin->get_errors()->has_errors() ) {
    325 			$error = $skin->get_errors();
    326 			$error->add_data( array( 'status' => 500 ) );
    327 
    328 			return $error;
    329 		}
    330 
    331 		if ( is_null( $result ) ) {
    332 			global $wp_filesystem;
    333 			// Pass through the error from WP_Filesystem if one was raised.
    334 			if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
    335 				return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), array( 'status' => 500 ) );
    336 			}
    337 
    338 			return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.' ), array( 'status' => 500 ) );
    339 		}
    340 
    341 		$file = $upgrader->plugin_info();
    342 
    343 		if ( ! $file ) {
    344 			return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.' ), array( 'status' => 500 ) );
    345 		}
    346 
    347 		if ( 'inactive' !== $request['status'] ) {
    348 			$can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' );
    349 
    350 			if ( is_wp_error( $can_change_status ) ) {
    351 				return $can_change_status;
    352 			}
    353 
    354 			$changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' );
    355 
    356 			if ( is_wp_error( $changed_status ) ) {
    357 				return $changed_status;
    358 			}
    359 		}
    360 
    361 		// Install translations.
    362 		$installed_locales = array_values( get_available_languages() );
    363 		/** This filter is documented in wp-includes/update.php */
    364 		$installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales );
    365 
    366 		$language_packs = array_map(
    367 			function( $item ) {
    368 				return (object) $item;
    369 			},
    370 			$api->language_packs
    371 		);
    372 
    373 		$language_packs = array_filter(
    374 			$language_packs,
    375 			function( $pack ) use ( $installed_locales ) {
    376 				return in_array( $pack->language, $installed_locales, true );
    377 			}
    378 		);
    379 
    380 		if ( $language_packs ) {
    381 			$lp_upgrader = new Language_Pack_Upgrader( $skin );
    382 
    383 			// Install all applicable language packs for the plugin.
    384 			$lp_upgrader->bulk_upgrade( $language_packs );
    385 		}
    386 
    387 		$path          = WP_PLUGIN_DIR . '/' . $file;
    388 		$data          = get_plugin_data( $path, false, false );
    389 		$data['_file'] = $file;
    390 
    391 		$response = $this->prepare_item_for_response( $data, $request );
    392 		$response->set_status( 201 );
    393 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) );
    394 
    395 		return $response;
    396 	}
    397 
    398 	/**
    399 	 * Checks if a given request has access to update a specific plugin.
    400 	 *
    401 	 * @since 5.5.0
    402 	 *
    403 	 * @param WP_REST_Request $request Full details about the request.
    404 	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
    405 	 */
    406 	public function update_item_permissions_check( $request ) {
    407 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    408 
    409 		if ( ! current_user_can( 'activate_plugins' ) ) {
    410 			return new WP_Error(
    411 				'rest_cannot_manage_plugins',
    412 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
    413 				array( 'status' => rest_authorization_required_code() )
    414 			);
    415 		}
    416 
    417 		$can_read = $this->check_read_permission( $request['plugin'] );
    418 
    419 		if ( is_wp_error( $can_read ) ) {
    420 			return $can_read;
    421 		}
    422 
    423 		$status = $this->get_plugin_status( $request['plugin'] );
    424 
    425 		if ( $request['status'] && $status !== $request['status'] ) {
    426 			$can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status );
    427 
    428 			if ( is_wp_error( $can_change_status ) ) {
    429 				return $can_change_status;
    430 			}
    431 		}
    432 
    433 		return true;
    434 	}
    435 
    436 	/**
    437 	 * Updates one plugin.
    438 	 *
    439 	 * @since 5.5.0
    440 	 *
    441 	 * @param WP_REST_Request $request Full details about the request.
    442 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    443 	 */
    444 	public function update_item( $request ) {
    445 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    446 
    447 		$data = $this->get_plugin_data( $request['plugin'] );
    448 
    449 		if ( is_wp_error( $data ) ) {
    450 			return $data;
    451 		}
    452 
    453 		$status = $this->get_plugin_status( $request['plugin'] );
    454 
    455 		if ( $request['status'] && $status !== $request['status'] ) {
    456 			$handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status );
    457 
    458 			if ( is_wp_error( $handled ) ) {
    459 				return $handled;
    460 			}
    461 		}
    462 
    463 		$this->update_additional_fields_for_object( $data, $request );
    464 
    465 		$request['context'] = 'edit';
    466 
    467 		return $this->prepare_item_for_response( $data, $request );
    468 	}
    469 
    470 	/**
    471 	 * Checks if a given request has access to delete a specific plugin.
    472 	 *
    473 	 * @since 5.5.0
    474 	 *
    475 	 * @param WP_REST_Request $request Full details about the request.
    476 	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
    477 	 */
    478 	public function delete_item_permissions_check( $request ) {
    479 		if ( ! current_user_can( 'activate_plugins' ) ) {
    480 			return new WP_Error(
    481 				'rest_cannot_manage_plugins',
    482 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
    483 				array( 'status' => rest_authorization_required_code() )
    484 			);
    485 		}
    486 
    487 		if ( ! current_user_can( 'delete_plugins' ) ) {
    488 			return new WP_Error(
    489 				'rest_cannot_manage_plugins',
    490 				__( 'Sorry, you are not allowed to delete plugins for this site.' ),
    491 				array( 'status' => rest_authorization_required_code() )
    492 			);
    493 		}
    494 
    495 		$can_read = $this->check_read_permission( $request['plugin'] );
    496 
    497 		if ( is_wp_error( $can_read ) ) {
    498 			return $can_read;
    499 		}
    500 
    501 		return true;
    502 	}
    503 
    504 	/**
    505 	 * Deletes one plugin from the site.
    506 	 *
    507 	 * @since 5.5.0
    508 	 *
    509 	 * @param WP_REST_Request $request Full details about the request.
    510 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    511 	 */
    512 	public function delete_item( $request ) {
    513 		require_once ABSPATH . 'wp-admin/includes/file.php';
    514 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
    515 
    516 		$data = $this->get_plugin_data( $request['plugin'] );
    517 
    518 		if ( is_wp_error( $data ) ) {
    519 			return $data;
    520 		}
    521 
    522 		if ( is_plugin_active( $request['plugin'] ) ) {
    523 			return new WP_Error(
    524 				'rest_cannot_delete_active_plugin',
    525 				__( 'Cannot delete an active plugin. Please deactivate it first.' ),
    526 				array( 'status' => 400 )
    527 			);
    528 		}
    529 
    530 		$filesystem_available = $this->is_filesystem_available();
    531 		if ( is_wp_error( $filesystem_available ) ) {
    532 			return $filesystem_available;
    533 		}
    534 
    535 		$prepared = $this->prepare_item_for_response( $data, $request );
    536 		$deleted  = delete_plugins( array( $request['plugin'] ) );
    537 
    538 		if ( is_wp_error( $deleted ) ) {
    539 			$deleted->add_data( array( 'status' => 500 ) );
    540 
    541 			return $deleted;
    542 		}
    543 
    544 		return new WP_REST_Response(
    545 			array(
    546 				'deleted'  => true,
    547 				'previous' => $prepared->get_data(),
    548 			)
    549 		);
    550 	}
    551 
    552 	/**
    553 	 * Prepares the plugin for the REST response.
    554 	 *
    555 	 * @since 5.5.0
    556 	 *
    557 	 * @param mixed           $item    Unmarked up and untranslated plugin data from {@see get_plugin_data()}.
    558 	 * @param WP_REST_Request $request Request object.
    559 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    560 	 */
    561 	public function prepare_item_for_response( $item, $request ) {
    562 		$item   = _get_plugin_data_markup_translate( $item['_file'], $item, false );
    563 		$marked = _get_plugin_data_markup_translate( $item['_file'], $item, true );
    564 
    565 		$data = array(
    566 			'plugin'       => substr( $item['_file'], 0, - 4 ),
    567 			'status'       => $this->get_plugin_status( $item['_file'] ),
    568 			'name'         => $item['Name'],
    569 			'plugin_uri'   => $item['PluginURI'],
    570 			'author'       => $item['Author'],
    571 			'author_uri'   => $item['AuthorURI'],
    572 			'description'  => array(
    573 				'raw'      => $item['Description'],
    574 				'rendered' => $marked['Description'],
    575 			),
    576 			'version'      => $item['Version'],
    577 			'network_only' => $item['Network'],
    578 			'requires_wp'  => $item['RequiresWP'],
    579 			'requires_php' => $item['RequiresPHP'],
    580 			'textdomain'   => $item['TextDomain'],
    581 		);
    582 
    583 		$data = $this->add_additional_fields_to_object( $data, $request );
    584 
    585 		$response = new WP_REST_Response( $data );
    586 		$response->add_links( $this->prepare_links( $item ) );
    587 
    588 		/**
    589 		 * Filters plugin data for a REST API response.
    590 		 *
    591 		 * @since 5.5.0
    592 		 *
    593 		 * @param WP_REST_Response $response The response object.
    594 		 * @param array            $item     The plugin item from {@see get_plugin_data()}.
    595 		 * @param WP_REST_Request  $request  The request object.
    596 		 */
    597 		return apply_filters( 'rest_prepare_plugin', $response, $item, $request );
    598 	}
    599 
    600 	/**
    601 	 * Prepares links for the request.
    602 	 *
    603 	 * @since 5.5.0
    604 	 *
    605 	 * @param array $item The plugin item.
    606 	 * @return array[]
    607 	 */
    608 	protected function prepare_links( $item ) {
    609 		return array(
    610 			'self' => array(
    611 				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ),
    612 			),
    613 		);
    614 	}
    615 
    616 	/**
    617 	 * Gets the plugin header data for a plugin.
    618 	 *
    619 	 * @since 5.5.0
    620 	 *
    621 	 * @param string $plugin The plugin file to get data for.
    622 	 * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed.
    623 	 */
    624 	protected function get_plugin_data( $plugin ) {
    625 		$plugins = get_plugins();
    626 
    627 		if ( ! isset( $plugins[ $plugin ] ) ) {
    628 			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
    629 		}
    630 
    631 		$data          = $plugins[ $plugin ];
    632 		$data['_file'] = $plugin;
    633 
    634 		return $data;
    635 	}
    636 
    637 	/**
    638 	 * Get's the activation status for a plugin.
    639 	 *
    640 	 * @since 5.5.0
    641 	 *
    642 	 * @param string $plugin The plugin file to check.
    643 	 * @return string Either 'network-active', 'active' or 'inactive'.
    644 	 */
    645 	protected function get_plugin_status( $plugin ) {
    646 		if ( is_plugin_active_for_network( $plugin ) ) {
    647 			return 'network-active';
    648 		}
    649 
    650 		if ( is_plugin_active( $plugin ) ) {
    651 			return 'active';
    652 		}
    653 
    654 		return 'inactive';
    655 	}
    656 
    657 	/**
    658 	 * Handle updating a plugin's status.
    659 	 *
    660 	 * @since 5.5.0
    661 	 *
    662 	 * @param string $plugin         The plugin file to update.
    663 	 * @param string $new_status     The plugin's new status.
    664 	 * @param string $current_status The plugin's current status.
    665 	 * @return true|WP_Error
    666 	 */
    667 	protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) {
    668 		if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) {
    669 			return new WP_Error(
    670 				'rest_cannot_manage_network_plugins',
    671 				__( 'Sorry, you are not allowed to manage network plugins.' ),
    672 				array( 'status' => rest_authorization_required_code() )
    673 			);
    674 		}
    675 
    676 		if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) {
    677 			return new WP_Error(
    678 				'rest_cannot_activate_plugin',
    679 				__( 'Sorry, you are not allowed to activate this plugin.' ),
    680 				array( 'status' => rest_authorization_required_code() )
    681 			);
    682 		}
    683 
    684 		if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) {
    685 			return new WP_Error(
    686 				'rest_cannot_deactivate_plugin',
    687 				__( 'Sorry, you are not allowed to deactivate this plugin.' ),
    688 				array( 'status' => rest_authorization_required_code() )
    689 			);
    690 		}
    691 
    692 		return true;
    693 	}
    694 
    695 	/**
    696 	 * Handle updating a plugin's status.
    697 	 *
    698 	 * @since 5.5.0
    699 	 *
    700 	 * @param string $plugin         The plugin file to update.
    701 	 * @param string $new_status     The plugin's new status.
    702 	 * @param string $current_status The plugin's current status.
    703 	 * @return true|WP_Error
    704 	 */
    705 	protected function handle_plugin_status( $plugin, $new_status, $current_status ) {
    706 		if ( 'inactive' === $new_status ) {
    707 			deactivate_plugins( $plugin, false, 'network-active' === $current_status );
    708 
    709 			return true;
    710 		}
    711 
    712 		if ( 'active' === $new_status && 'network-active' === $current_status ) {
    713 			return true;
    714 		}
    715 
    716 		$network_activate = 'network-active' === $new_status;
    717 
    718 		if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) {
    719 			return new WP_Error(
    720 				'rest_network_only_plugin',
    721 				__( 'Network only plugin must be network activated.' ),
    722 				array( 'status' => 400 )
    723 			);
    724 		}
    725 
    726 		$activated = activate_plugin( $plugin, '', $network_activate );
    727 
    728 		if ( is_wp_error( $activated ) ) {
    729 			$activated->add_data( array( 'status' => 500 ) );
    730 
    731 			return $activated;
    732 		}
    733 
    734 		return true;
    735 	}
    736 
    737 	/**
    738 	 * Checks that the "plugin" parameter is a valid path.
    739 	 *
    740 	 * @since 5.5.0
    741 	 *
    742 	 * @param string $file The plugin file parameter.
    743 	 * @return bool
    744 	 */
    745 	public function validate_plugin_param( $file ) {
    746 		if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) {
    747 			return false;
    748 		}
    749 
    750 		$validated = validate_file( plugin_basename( $file ) );
    751 
    752 		return 0 === $validated;
    753 	}
    754 
    755 	/**
    756 	 * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended.
    757 	 *
    758 	 * @since 5.5.0
    759 	 *
    760 	 * @param string $file The plugin file parameter.
    761 	 * @return string
    762 	 */
    763 	public function sanitize_plugin_param( $file ) {
    764 		return plugin_basename( sanitize_text_field( $file . '.php' ) );
    765 	}
    766 
    767 	/**
    768 	 * Checks if the plugin matches the requested parameters.
    769 	 *
    770 	 * @since 5.5.0
    771 	 *
    772 	 * @param WP_REST_Request $request The request to require the plugin matches against.
    773 	 * @param array           $item    The plugin item.
    774 	 * @return bool
    775 	 */
    776 	protected function does_plugin_match_request( $request, $item ) {
    777 		$search = $request['search'];
    778 
    779 		if ( $search ) {
    780 			$matched_search = false;
    781 
    782 			foreach ( $item as $field ) {
    783 				if ( is_string( $field ) && false !== strpos( strip_tags( $field ), $search ) ) {
    784 					$matched_search = true;
    785 					break;
    786 				}
    787 			}
    788 
    789 			if ( ! $matched_search ) {
    790 				return false;
    791 			}
    792 		}
    793 
    794 		$status = $request['status'];
    795 
    796 		if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) {
    797 			return false;
    798 		}
    799 
    800 		return true;
    801 	}
    802 
    803 	/**
    804 	 * Checks if the plugin is installed.
    805 	 *
    806 	 * @since 5.5.0
    807 	 *
    808 	 * @param string $plugin The plugin file.
    809 	 * @return bool
    810 	 */
    811 	protected function is_plugin_installed( $plugin ) {
    812 		return file_exists( WP_PLUGIN_DIR . '/' . $plugin );
    813 	}
    814 
    815 	/**
    816 	 * Determine if the endpoints are available.
    817 	 *
    818 	 * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present.
    819 	 *
    820 	 * @since 5.5.0
    821 	 *
    822 	 * @return true|WP_Error True if filesystem is available, WP_Error otherwise.
    823 	 */
    824 	protected function is_filesystem_available() {
    825 		$filesystem_method = get_filesystem_method();
    826 
    827 		if ( 'direct' === $filesystem_method ) {
    828 			return true;
    829 		}
    830 
    831 		ob_start();
    832 		$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
    833 		ob_end_clean();
    834 
    835 		if ( $filesystem_credentials_are_stored ) {
    836 			return true;
    837 		}
    838 
    839 		return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.' ), array( 'status' => 500 ) );
    840 	}
    841 
    842 	/**
    843 	 * Retrieves the plugin's schema, conforming to JSON Schema.
    844 	 *
    845 	 * @since 5.5.0
    846 	 *
    847 	 * @return array Item schema data.
    848 	 */
    849 	public function get_item_schema() {
    850 		if ( $this->schema ) {
    851 			return $this->add_additional_fields_schema( $this->schema );
    852 		}
    853 
    854 		$this->schema = array(
    855 			'$schema'    => 'http://json-schema.org/draft-04/schema#',
    856 			'title'      => 'plugin',
    857 			'type'       => 'object',
    858 			'properties' => array(
    859 				'plugin'       => array(
    860 					'description' => __( 'The plugin file.' ),
    861 					'type'        => 'string',
    862 					'pattern'     => self::PATTERN,
    863 					'readonly'    => true,
    864 					'context'     => array( 'view', 'edit', 'embed' ),
    865 				),
    866 				'status'       => array(
    867 					'description' => __( 'The plugin activation status.' ),
    868 					'type'        => 'string',
    869 					'enum'        => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
    870 					'context'     => array( 'view', 'edit', 'embed' ),
    871 				),
    872 				'name'         => array(
    873 					'description' => __( 'The plugin name.' ),
    874 					'type'        => 'string',
    875 					'readonly'    => true,
    876 					'context'     => array( 'view', 'edit', 'embed' ),
    877 				),
    878 				'plugin_uri'   => array(
    879 					'description' => __( 'The plugin\'s website address.' ),
    880 					'type'        => 'string',
    881 					'format'      => 'uri',
    882 					'readonly'    => true,
    883 					'context'     => array( 'view', 'edit' ),
    884 				),
    885 				'author'       => array(
    886 					'description' => __( 'The plugin author.' ),
    887 					'type'        => 'object',
    888 					'readonly'    => true,
    889 					'context'     => array( 'view', 'edit' ),
    890 				),
    891 				'author_uri'   => array(
    892 					'description' => __( 'Plugin author\'s website address.' ),
    893 					'type'        => 'string',
    894 					'format'      => 'uri',
    895 					'readonly'    => true,
    896 					'context'     => array( 'view', 'edit' ),
    897 				),
    898 				'description'  => array(
    899 					'description' => __( 'The plugin description.' ),
    900 					'type'        => 'object',
    901 					'readonly'    => true,
    902 					'context'     => array( 'view', 'edit' ),
    903 					'properties'  => array(
    904 						'raw'      => array(
    905 							'description' => __( 'The raw plugin description.' ),
    906 							'type'        => 'string',
    907 						),
    908 						'rendered' => array(
    909 							'description' => __( 'The plugin description formatted for display.' ),
    910 							'type'        => 'string',
    911 						),
    912 					),
    913 				),
    914 				'version'      => array(
    915 					'description' => __( 'The plugin version number.' ),
    916 					'type'        => 'string',
    917 					'readonly'    => true,
    918 					'context'     => array( 'view', 'edit' ),
    919 				),
    920 				'network_only' => array(
    921 					'description' => __( 'Whether the plugin can only be activated network-wide.' ),
    922 					'type'        => 'boolean',
    923 					'readonly'    => true,
    924 					'context'     => array( 'view', 'edit', 'embed' ),
    925 				),
    926 				'requires_wp'  => array(
    927 					'description' => __( 'Minimum required version of WordPress.' ),
    928 					'type'        => 'string',
    929 					'readonly'    => true,
    930 					'context'     => array( 'view', 'edit', 'embed' ),
    931 				),
    932 				'requires_php' => array(
    933 					'description' => __( 'Minimum required version of PHP.' ),
    934 					'type'        => 'string',
    935 					'readonly'    => true,
    936 					'context'     => array( 'view', 'edit', 'embed' ),
    937 				),
    938 				'textdomain'   => array(
    939 					'description' => __( 'The plugin\'s text domain.' ),
    940 					'type'        => 'string',
    941 					'readonly'    => true,
    942 					'context'     => array( 'view', 'edit' ),
    943 				),
    944 			),
    945 		);
    946 
    947 		return $this->add_additional_fields_schema( $this->schema );
    948 	}
    949 
    950 	/**
    951 	 * Retrieves the query params for the collections.
    952 	 *
    953 	 * @since 5.5.0
    954 	 *
    955 	 * @return array Query parameters for the collection.
    956 	 */
    957 	public function get_collection_params() {
    958 		$query_params = parent::get_collection_params();
    959 
    960 		$query_params['context']['default'] = 'view';
    961 
    962 		$query_params['status'] = array(
    963 			'description' => __( 'Limits results to plugins with the given status.' ),
    964 			'type'        => 'array',
    965 			'items'       => array(
    966 				'type' => 'string',
    967 				'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
    968 			),
    969 		);
    970 
    971 		unset( $query_params['page'], $query_params['per_page'] );
    972 
    973 		return $query_params;
    974 	}
    975 }