wp/wp-includes/rest-api/endpoints/class-wp-rest-plugins-controller.php
changeset 16 a86126ab1dd4
child 18 be944660c56a
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
       
     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 		if ( ! $this->is_plugin_installed( $plugin ) ) {
       
   213 			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
       
   214 		}
       
   215 
       
   216 		if ( ! is_multisite() ) {
       
   217 			return true;
       
   218 		}
       
   219 
       
   220 		if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) {
       
   221 			return true;
       
   222 		}
       
   223 
       
   224 		return new WP_Error(
       
   225 			'rest_cannot_view_plugin',
       
   226 			__( 'Sorry, you are not allowed to manage this plugin.' ),
       
   227 			array( 'status' => rest_authorization_required_code() )
       
   228 		);
       
   229 	}
       
   230 
       
   231 	/**
       
   232 	 * Checks if a given request has access to upload plugins.
       
   233 	 *
       
   234 	 * @since 5.5.0
       
   235 	 *
       
   236 	 * @param WP_REST_Request $request Full details about the request.
       
   237 	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
       
   238 	 */
       
   239 	public function create_item_permissions_check( $request ) {
       
   240 		if ( ! current_user_can( 'install_plugins' ) ) {
       
   241 			return new WP_Error(
       
   242 				'rest_cannot_install_plugin',
       
   243 				__( 'Sorry, you are not allowed to install plugins on this site.' ),
       
   244 				array( 'status' => rest_authorization_required_code() )
       
   245 			);
       
   246 		}
       
   247 
       
   248 		if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) {
       
   249 			return new WP_Error(
       
   250 				'rest_cannot_activate_plugin',
       
   251 				__( 'Sorry, you are not allowed to activate plugins.' ),
       
   252 				array(
       
   253 					'status' => rest_authorization_required_code(),
       
   254 				)
       
   255 			);
       
   256 		}
       
   257 
       
   258 		return true;
       
   259 	}
       
   260 
       
   261 	/**
       
   262 	 * Uploads a plugin and optionally activates it.
       
   263 	 *
       
   264 	 * @since 5.5.0
       
   265 	 *
       
   266 	 * @param WP_REST_Request $request Full details about the request.
       
   267 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   268 	 */
       
   269 	public function create_item( $request ) {
       
   270 		require_once ABSPATH . 'wp-admin/includes/file.php';
       
   271 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
       
   272 		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
       
   273 		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
       
   274 
       
   275 		$slug = $request['slug'];
       
   276 
       
   277 		// Verify filesystem is accessible first.
       
   278 		$filesystem_available = $this->is_filesystem_available();
       
   279 		if ( is_wp_error( $filesystem_available ) ) {
       
   280 			return $filesystem_available;
       
   281 		}
       
   282 
       
   283 		$api = plugins_api(
       
   284 			'plugin_information',
       
   285 			array(
       
   286 				'slug'   => $slug,
       
   287 				'fields' => array(
       
   288 					'sections'       => false,
       
   289 					'language_packs' => true,
       
   290 				),
       
   291 			)
       
   292 		);
       
   293 
       
   294 		if ( is_wp_error( $api ) ) {
       
   295 			if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) {
       
   296 				$api->add_data( array( 'status' => 404 ) );
       
   297 			} else {
       
   298 				$api->add_data( array( 'status' => 500 ) );
       
   299 			}
       
   300 
       
   301 			return $api;
       
   302 		}
       
   303 
       
   304 		$skin     = new WP_Ajax_Upgrader_Skin();
       
   305 		$upgrader = new Plugin_Upgrader( $skin );
       
   306 
       
   307 		$result = $upgrader->install( $api->download_link );
       
   308 
       
   309 		if ( is_wp_error( $result ) ) {
       
   310 			$result->add_data( array( 'status' => 500 ) );
       
   311 
       
   312 			return $result;
       
   313 		}
       
   314 
       
   315 		// This should be the same as $result above.
       
   316 		if ( is_wp_error( $skin->result ) ) {
       
   317 			$skin->result->add_data( array( 'status' => 500 ) );
       
   318 
       
   319 			return $skin->result;
       
   320 		}
       
   321 
       
   322 		if ( $skin->get_errors()->has_errors() ) {
       
   323 			$error = $skin->get_errors();
       
   324 			$error->add_data( array( 'status' => 500 ) );
       
   325 
       
   326 			return $error;
       
   327 		}
       
   328 
       
   329 		if ( is_null( $result ) ) {
       
   330 			global $wp_filesystem;
       
   331 			// Pass through the error from WP_Filesystem if one was raised.
       
   332 			if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
       
   333 				return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), array( 'status' => 500 ) );
       
   334 			}
       
   335 
       
   336 			return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.' ), array( 'status' => 500 ) );
       
   337 		}
       
   338 
       
   339 		$file = $upgrader->plugin_info();
       
   340 
       
   341 		if ( ! $file ) {
       
   342 			return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.' ), array( 'status' => 500 ) );
       
   343 		}
       
   344 
       
   345 		if ( 'inactive' !== $request['status'] ) {
       
   346 			$can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' );
       
   347 
       
   348 			if ( is_wp_error( $can_change_status ) ) {
       
   349 				return $can_change_status;
       
   350 			}
       
   351 
       
   352 			$changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' );
       
   353 
       
   354 			if ( is_wp_error( $changed_status ) ) {
       
   355 				return $changed_status;
       
   356 			}
       
   357 		}
       
   358 
       
   359 		// Install translations.
       
   360 		$installed_locales = array_values( get_available_languages() );
       
   361 		/** This filter is documented in wp-includes/update.php */
       
   362 		$installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales );
       
   363 
       
   364 		$language_packs = array_map(
       
   365 			function( $item ) {
       
   366 				return (object) $item;
       
   367 			},
       
   368 			$api->language_packs
       
   369 		);
       
   370 
       
   371 		$language_packs = array_filter(
       
   372 			$language_packs,
       
   373 			function( $pack ) use ( $installed_locales ) {
       
   374 				return in_array( $pack->language, $installed_locales, true );
       
   375 			}
       
   376 		);
       
   377 
       
   378 		if ( $language_packs ) {
       
   379 			$lp_upgrader = new Language_Pack_Upgrader( $skin );
       
   380 
       
   381 			// Install all applicable language packs for the plugin.
       
   382 			$lp_upgrader->bulk_upgrade( $language_packs );
       
   383 		}
       
   384 
       
   385 		$path          = WP_PLUGIN_DIR . '/' . $file;
       
   386 		$data          = get_plugin_data( $path, false, false );
       
   387 		$data['_file'] = $file;
       
   388 
       
   389 		$response = $this->prepare_item_for_response( $data, $request );
       
   390 		$response->set_status( 201 );
       
   391 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) );
       
   392 
       
   393 		return $response;
       
   394 	}
       
   395 
       
   396 	/**
       
   397 	 * Checks if a given request has access to update a specific plugin.
       
   398 	 *
       
   399 	 * @since 5.5.0
       
   400 	 *
       
   401 	 * @param WP_REST_Request $request Full details about the request.
       
   402 	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
       
   403 	 */
       
   404 	public function update_item_permissions_check( $request ) {
       
   405 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
       
   406 
       
   407 		if ( ! current_user_can( 'activate_plugins' ) ) {
       
   408 			return new WP_Error(
       
   409 				'rest_cannot_manage_plugins',
       
   410 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
       
   411 				array( 'status' => rest_authorization_required_code() )
       
   412 			);
       
   413 		}
       
   414 
       
   415 		$can_read = $this->check_read_permission( $request['plugin'] );
       
   416 
       
   417 		if ( is_wp_error( $can_read ) ) {
       
   418 			return $can_read;
       
   419 		}
       
   420 
       
   421 		$status = $this->get_plugin_status( $request['plugin'] );
       
   422 
       
   423 		if ( $request['status'] && $status !== $request['status'] ) {
       
   424 			$can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status );
       
   425 
       
   426 			if ( is_wp_error( $can_change_status ) ) {
       
   427 				return $can_change_status;
       
   428 			}
       
   429 		}
       
   430 
       
   431 		return true;
       
   432 	}
       
   433 
       
   434 	/**
       
   435 	 * Updates one plugin.
       
   436 	 *
       
   437 	 * @since 5.5.0
       
   438 	 *
       
   439 	 * @param WP_REST_Request $request Full details about the request.
       
   440 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   441 	 */
       
   442 	public function update_item( $request ) {
       
   443 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
       
   444 
       
   445 		$data = $this->get_plugin_data( $request['plugin'] );
       
   446 
       
   447 		if ( is_wp_error( $data ) ) {
       
   448 			return $data;
       
   449 		}
       
   450 
       
   451 		$status = $this->get_plugin_status( $request['plugin'] );
       
   452 
       
   453 		if ( $request['status'] && $status !== $request['status'] ) {
       
   454 			$handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status );
       
   455 
       
   456 			if ( is_wp_error( $handled ) ) {
       
   457 				return $handled;
       
   458 			}
       
   459 		}
       
   460 
       
   461 		$this->update_additional_fields_for_object( $data, $request );
       
   462 
       
   463 		$request['context'] = 'edit';
       
   464 
       
   465 		return $this->prepare_item_for_response( $data, $request );
       
   466 	}
       
   467 
       
   468 	/**
       
   469 	 * Checks if a given request has access to delete a specific plugin.
       
   470 	 *
       
   471 	 * @since 5.5.0
       
   472 	 *
       
   473 	 * @param WP_REST_Request $request Full details about the request.
       
   474 	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
       
   475 	 */
       
   476 	public function delete_item_permissions_check( $request ) {
       
   477 		if ( ! current_user_can( 'activate_plugins' ) ) {
       
   478 			return new WP_Error(
       
   479 				'rest_cannot_manage_plugins',
       
   480 				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
       
   481 				array( 'status' => rest_authorization_required_code() )
       
   482 			);
       
   483 		}
       
   484 
       
   485 		if ( ! current_user_can( 'delete_plugins' ) ) {
       
   486 			return new WP_Error(
       
   487 				'rest_cannot_manage_plugins',
       
   488 				__( 'Sorry, you are not allowed to delete plugins for this site.' ),
       
   489 				array( 'status' => rest_authorization_required_code() )
       
   490 			);
       
   491 		}
       
   492 
       
   493 		$can_read = $this->check_read_permission( $request['plugin'] );
       
   494 
       
   495 		if ( is_wp_error( $can_read ) ) {
       
   496 			return $can_read;
       
   497 		}
       
   498 
       
   499 		return true;
       
   500 	}
       
   501 
       
   502 	/**
       
   503 	 * Deletes one plugin from the site.
       
   504 	 *
       
   505 	 * @since 5.5.0
       
   506 	 *
       
   507 	 * @param WP_REST_Request $request Full details about the request.
       
   508 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   509 	 */
       
   510 	public function delete_item( $request ) {
       
   511 		require_once ABSPATH . 'wp-admin/includes/file.php';
       
   512 		require_once ABSPATH . 'wp-admin/includes/plugin.php';
       
   513 
       
   514 		$data = $this->get_plugin_data( $request['plugin'] );
       
   515 
       
   516 		if ( is_wp_error( $data ) ) {
       
   517 			return $data;
       
   518 		}
       
   519 
       
   520 		if ( is_plugin_active( $request['plugin'] ) ) {
       
   521 			return new WP_Error(
       
   522 				'rest_cannot_delete_active_plugin',
       
   523 				__( 'Cannot delete an active plugin. Please deactivate it first.' ),
       
   524 				array( 'status' => 400 )
       
   525 			);
       
   526 		}
       
   527 
       
   528 		$filesystem_available = $this->is_filesystem_available();
       
   529 		if ( is_wp_error( $filesystem_available ) ) {
       
   530 			return $filesystem_available;
       
   531 		}
       
   532 
       
   533 		$prepared = $this->prepare_item_for_response( $data, $request );
       
   534 		$deleted  = delete_plugins( array( $request['plugin'] ) );
       
   535 
       
   536 		if ( is_wp_error( $deleted ) ) {
       
   537 			$deleted->add_data( array( 'status' => 500 ) );
       
   538 
       
   539 			return $deleted;
       
   540 		}
       
   541 
       
   542 		return new WP_REST_Response(
       
   543 			array(
       
   544 				'deleted'  => true,
       
   545 				'previous' => $prepared->get_data(),
       
   546 			)
       
   547 		);
       
   548 	}
       
   549 
       
   550 	/**
       
   551 	 * Prepares the plugin for the REST response.
       
   552 	 *
       
   553 	 * @since 5.5.0
       
   554 	 *
       
   555 	 * @param mixed           $item    Unmarked up and untranslated plugin data from {@see get_plugin_data()}.
       
   556 	 * @param WP_REST_Request $request Request object.
       
   557 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   558 	 */
       
   559 	public function prepare_item_for_response( $item, $request ) {
       
   560 		$item   = _get_plugin_data_markup_translate( $item['_file'], $item, false );
       
   561 		$marked = _get_plugin_data_markup_translate( $item['_file'], $item, true );
       
   562 
       
   563 		$data = array(
       
   564 			'plugin'       => substr( $item['_file'], 0, - 4 ),
       
   565 			'status'       => $this->get_plugin_status( $item['_file'] ),
       
   566 			'name'         => $item['Name'],
       
   567 			'plugin_uri'   => $item['PluginURI'],
       
   568 			'author'       => $item['Author'],
       
   569 			'author_uri'   => $item['AuthorURI'],
       
   570 			'description'  => array(
       
   571 				'raw'      => $item['Description'],
       
   572 				'rendered' => $marked['Description'],
       
   573 			),
       
   574 			'version'      => $item['Version'],
       
   575 			'network_only' => $item['Network'],
       
   576 			'requires_wp'  => $item['RequiresWP'],
       
   577 			'requires_php' => $item['RequiresPHP'],
       
   578 			'textdomain'   => $item['TextDomain'],
       
   579 		);
       
   580 
       
   581 		$data = $this->add_additional_fields_to_object( $data, $request );
       
   582 
       
   583 		$response = new WP_REST_Response( $data );
       
   584 		$response->add_links( $this->prepare_links( $item ) );
       
   585 
       
   586 		/**
       
   587 		 * Filters the plugin data for a response.
       
   588 		 *
       
   589 		 * @since 5.5.0
       
   590 		 *
       
   591 		 * @param WP_REST_Response $response The response object.
       
   592 		 * @param array            $item     The plugin item from {@see get_plugin_data()}.
       
   593 		 * @param WP_REST_Request  $request  The request object.
       
   594 		 */
       
   595 		return apply_filters( 'rest_prepare_plugin', $response, $item, $request );
       
   596 	}
       
   597 
       
   598 	/**
       
   599 	 * Prepares links for the request.
       
   600 	 *
       
   601 	 * @since 5.5.0
       
   602 	 *
       
   603 	 * @param array $item The plugin item.
       
   604 	 * @return array[]
       
   605 	 */
       
   606 	protected function prepare_links( $item ) {
       
   607 		return array(
       
   608 			'self' => array(
       
   609 				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ),
       
   610 			),
       
   611 		);
       
   612 	}
       
   613 
       
   614 	/**
       
   615 	 * Gets the plugin header data for a plugin.
       
   616 	 *
       
   617 	 * @since 5.5.0
       
   618 	 *
       
   619 	 * @param string $plugin The plugin file to get data for.
       
   620 	 * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed.
       
   621 	 */
       
   622 	protected function get_plugin_data( $plugin ) {
       
   623 		$plugins = get_plugins();
       
   624 
       
   625 		if ( ! isset( $plugins[ $plugin ] ) ) {
       
   626 			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
       
   627 		}
       
   628 
       
   629 		$data          = $plugins[ $plugin ];
       
   630 		$data['_file'] = $plugin;
       
   631 
       
   632 		return $data;
       
   633 	}
       
   634 
       
   635 	/**
       
   636 	 * Get's the activation status for a plugin.
       
   637 	 *
       
   638 	 * @since 5.5.0
       
   639 	 *
       
   640 	 * @param string $plugin The plugin file to check.
       
   641 	 * @return string Either 'network-active', 'active' or 'inactive'.
       
   642 	 */
       
   643 	protected function get_plugin_status( $plugin ) {
       
   644 		if ( is_plugin_active_for_network( $plugin ) ) {
       
   645 			return 'network-active';
       
   646 		}
       
   647 
       
   648 		if ( is_plugin_active( $plugin ) ) {
       
   649 			return 'active';
       
   650 		}
       
   651 
       
   652 		return 'inactive';
       
   653 	}
       
   654 
       
   655 	/**
       
   656 	 * Handle updating a plugin's status.
       
   657 	 *
       
   658 	 * @since 5.5.0
       
   659 	 *
       
   660 	 * @param string $plugin         The plugin file to update.
       
   661 	 * @param string $new_status     The plugin's new status.
       
   662 	 * @param string $current_status The plugin's current status.
       
   663 	 *
       
   664 	 * @return true|WP_Error
       
   665 	 */
       
   666 	protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) {
       
   667 		if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) {
       
   668 			return new WP_Error(
       
   669 				'rest_cannot_manage_network_plugins',
       
   670 				__( 'Sorry, you are not allowed to manage network plugins.' ),
       
   671 				array( 'status' => rest_authorization_required_code() )
       
   672 			);
       
   673 		}
       
   674 
       
   675 		if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) {
       
   676 			return new WP_Error(
       
   677 				'rest_cannot_activate_plugin',
       
   678 				__( 'Sorry, you are not allowed to activate this plugin.' ),
       
   679 				array( 'status' => rest_authorization_required_code() )
       
   680 			);
       
   681 		}
       
   682 
       
   683 		if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) {
       
   684 			return new WP_Error(
       
   685 				'rest_cannot_deactivate_plugin',
       
   686 				__( 'Sorry, you are not allowed to deactivate this plugin.' ),
       
   687 				array( 'status' => rest_authorization_required_code() )
       
   688 			);
       
   689 		}
       
   690 
       
   691 		return true;
       
   692 	}
       
   693 
       
   694 	/**
       
   695 	 * Handle updating a plugin's status.
       
   696 	 *
       
   697 	 * @since 5.5.0
       
   698 	 *
       
   699 	 * @param string $plugin         The plugin file to update.
       
   700 	 * @param string $new_status     The plugin's new status.
       
   701 	 * @param string $current_status The plugin's current status.
       
   702 	 * @return true|WP_Error
       
   703 	 */
       
   704 	protected function handle_plugin_status( $plugin, $new_status, $current_status ) {
       
   705 		if ( 'inactive' === $new_status ) {
       
   706 			deactivate_plugins( $plugin, false, 'network-active' === $current_status );
       
   707 
       
   708 			return true;
       
   709 		}
       
   710 
       
   711 		if ( 'active' === $new_status && 'network-active' === $current_status ) {
       
   712 			return true;
       
   713 		}
       
   714 
       
   715 		$network_activate = 'network-active' === $new_status;
       
   716 
       
   717 		if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) {
       
   718 			return new WP_Error(
       
   719 				'rest_network_only_plugin',
       
   720 				__( 'Network only plugin must be network activated.' ),
       
   721 				array( 'status' => 400 )
       
   722 			);
       
   723 		}
       
   724 
       
   725 		$activated = activate_plugin( $plugin, '', $network_activate );
       
   726 
       
   727 		if ( is_wp_error( $activated ) ) {
       
   728 			$activated->add_data( array( 'status' => 500 ) );
       
   729 
       
   730 			return $activated;
       
   731 		}
       
   732 
       
   733 		return true;
       
   734 	}
       
   735 
       
   736 	/**
       
   737 	 * Checks that the "plugin" parameter is a valid path.
       
   738 	 *
       
   739 	 * @since 5.5.0
       
   740 	 *
       
   741 	 * @param string $file The plugin file parameter.
       
   742 	 * @return bool
       
   743 	 */
       
   744 	public function validate_plugin_param( $file ) {
       
   745 		if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) {
       
   746 			return false;
       
   747 		}
       
   748 
       
   749 		$validated = validate_file( plugin_basename( $file ) );
       
   750 
       
   751 		return 0 === $validated;
       
   752 	}
       
   753 
       
   754 	/**
       
   755 	 * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended.
       
   756 	 *
       
   757 	 * @since 5.5.0
       
   758 	 *
       
   759 	 * @param string $file The plugin file parameter.
       
   760 	 * @return string
       
   761 	 */
       
   762 	public function sanitize_plugin_param( $file ) {
       
   763 		return plugin_basename( sanitize_text_field( $file . '.php' ) );
       
   764 	}
       
   765 
       
   766 	/**
       
   767 	 * Checks if the plugin matches the requested parameters.
       
   768 	 *
       
   769 	 * @since 5.5.0
       
   770 	 *
       
   771 	 * @param WP_REST_Request $request The request to require the plugin matches against.
       
   772 	 * @param array           $item    The plugin item.
       
   773 	 *
       
   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 }