No Description

class-wp-rest-plugins-controller.php 28KB

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