class-wc-rest-system-status-tools-controller.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <?php
  2. /**
  3. * REST API WC System Status Tools Controller
  4. *
  5. * Handles requests to the /system_status/tools/* endpoints.
  6. *
  7. * @package WooCommerce/API
  8. * @since 3.0.0
  9. */
  10. defined( 'ABSPATH' ) || exit;
  11. /**
  12. * System status tools controller.
  13. *
  14. * @package WooCommerce/API
  15. * @extends WC_REST_Controller
  16. */
  17. class WC_REST_System_Status_Tools_Controller extends WC_REST_Controller {
  18. /**
  19. * Endpoint namespace.
  20. *
  21. * @var string
  22. */
  23. protected $namespace = 'wc/v2';
  24. /**
  25. * Route base.
  26. *
  27. * @var string
  28. */
  29. protected $rest_base = 'system_status/tools';
  30. /**
  31. * Register the routes for /system_status/tools/*.
  32. */
  33. public function register_routes() {
  34. register_rest_route(
  35. $this->namespace, '/' . $this->rest_base, array(
  36. array(
  37. 'methods' => WP_REST_Server::READABLE,
  38. 'callback' => array( $this, 'get_items' ),
  39. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  40. 'args' => $this->get_collection_params(),
  41. ),
  42. 'schema' => array( $this, 'get_public_item_schema' ),
  43. )
  44. );
  45. register_rest_route(
  46. $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
  47. 'args' => array(
  48. 'id' => array(
  49. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
  50. 'type' => 'string',
  51. ),
  52. ),
  53. array(
  54. 'methods' => WP_REST_Server::READABLE,
  55. 'callback' => array( $this, 'get_item' ),
  56. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  57. ),
  58. array(
  59. 'methods' => WP_REST_Server::EDITABLE,
  60. 'callback' => array( $this, 'update_item' ),
  61. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  62. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  63. ),
  64. 'schema' => array( $this, 'get_public_item_schema' ),
  65. )
  66. );
  67. }
  68. /**
  69. * Check whether a given request has permission to view system status tools.
  70. *
  71. * @param WP_REST_Request $request Full details about the request.
  72. * @return WP_Error|boolean
  73. */
  74. public function get_items_permissions_check( $request ) {
  75. if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
  76. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  77. }
  78. return true;
  79. }
  80. /**
  81. * Check whether a given request has permission to view a specific system status tool.
  82. *
  83. * @param WP_REST_Request $request Full details about the request.
  84. * @return WP_Error|boolean
  85. */
  86. public function get_item_permissions_check( $request ) {
  87. if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
  88. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  89. }
  90. return true;
  91. }
  92. /**
  93. * Check whether a given request has permission to execute a specific system status tool.
  94. *
  95. * @param WP_REST_Request $request Full details about the request.
  96. * @return WP_Error|boolean
  97. */
  98. public function update_item_permissions_check( $request ) {
  99. if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) {
  100. return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  101. }
  102. return true;
  103. }
  104. /**
  105. * A list of available tools for use in the system status section.
  106. * 'button' becomes 'action' in the API.
  107. *
  108. * @return array
  109. */
  110. public function get_tools() {
  111. $tools = array(
  112. 'clear_transients' => array(
  113. 'name' => __( 'WooCommerce transients', 'woocommerce' ),
  114. 'button' => __( 'Clear transients', 'woocommerce' ),
  115. 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ),
  116. ),
  117. 'clear_expired_transients' => array(
  118. 'name' => __( 'Expired transients', 'woocommerce' ),
  119. 'button' => __( 'Clear transients', 'woocommerce' ),
  120. 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ),
  121. ),
  122. 'delete_orphaned_variations' => array(
  123. 'name' => __( 'Orphaned variations', 'woocommerce' ),
  124. 'button' => __( 'Delete orphaned variations', 'woocommerce' ),
  125. 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ),
  126. ),
  127. 'clear_expired_download_permissions' => array(
  128. 'name' => __( 'Used-up download permissions', 'woocommerce' ),
  129. 'button' => __( 'Clean up download permissions', 'woocommerce' ),
  130. 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ),
  131. ),
  132. 'add_order_indexes' => array(
  133. 'name' => __( 'Order address indexes', 'woocommerce' ),
  134. 'button' => __( 'Index orders', 'woocommerce' ),
  135. 'desc' => __( 'This tool will add address indexes to orders that do not have them yet. This improves order search results.', 'woocommerce' ),
  136. ),
  137. 'recount_terms' => array(
  138. 'name' => __( 'Term counts', 'woocommerce' ),
  139. 'button' => __( 'Recount terms', 'woocommerce' ),
  140. 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ),
  141. ),
  142. 'reset_roles' => array(
  143. 'name' => __( 'Capabilities', 'woocommerce' ),
  144. 'button' => __( 'Reset capabilities', 'woocommerce' ),
  145. 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ),
  146. ),
  147. 'clear_sessions' => array(
  148. 'name' => __( 'Clear customer sessions', 'woocommerce' ),
  149. 'button' => __( 'Clear', 'woocommerce' ),
  150. 'desc' => sprintf(
  151. '<strong class="red">%1$s</strong> %2$s',
  152. __( 'Note:', 'woocommerce' ),
  153. __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' )
  154. ),
  155. ),
  156. 'install_pages' => array(
  157. 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ),
  158. 'button' => __( 'Create pages', 'woocommerce' ),
  159. 'desc' => sprintf(
  160. '<strong class="red">%1$s</strong> %2$s',
  161. __( 'Note:', 'woocommerce' ),
  162. __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' )
  163. ),
  164. ),
  165. 'delete_taxes' => array(
  166. 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ),
  167. 'button' => __( 'Delete tax rates', 'woocommerce' ),
  168. 'desc' => sprintf(
  169. '<strong class="red">%1$s</strong> %2$s',
  170. __( 'Note:', 'woocommerce' ),
  171. __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' )
  172. ),
  173. ),
  174. 'reset_tracking' => array(
  175. 'name' => __( 'Reset usage tracking', 'woocommerce' ),
  176. 'button' => __( 'Reset', 'woocommerce' ),
  177. 'desc' => __( 'This will reset your usage tracking settings, causing it to show the opt-in banner again and not sending any data.', 'woocommerce' ),
  178. ),
  179. 'regenerate_thumbnails' => array(
  180. 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ),
  181. 'button' => __( 'Regenerate', 'woocommerce' ),
  182. 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ),
  183. ),
  184. );
  185. // Jetpack does the image resizing heavy lifting so you don't have to.
  186. if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) {
  187. unset( $tools['regenerate_thumbnails'] );
  188. }
  189. return apply_filters( 'woocommerce_debug_tools', $tools );
  190. }
  191. /**
  192. * Get a list of system status tools.
  193. *
  194. * @param WP_REST_Request $request Full details about the request.
  195. * @return WP_Error|WP_REST_Response
  196. */
  197. public function get_items( $request ) {
  198. $tools = array();
  199. foreach ( $this->get_tools() as $id => $tool ) {
  200. $tools[] = $this->prepare_response_for_collection(
  201. $this->prepare_item_for_response(
  202. array(
  203. 'id' => $id,
  204. 'name' => $tool['name'],
  205. 'action' => $tool['button'],
  206. 'description' => $tool['desc'],
  207. ), $request
  208. )
  209. );
  210. }
  211. $response = rest_ensure_response( $tools );
  212. return $response;
  213. }
  214. /**
  215. * Return a single tool.
  216. *
  217. * @param WP_REST_Request $request Request data.
  218. * @return WP_Error|WP_REST_Response
  219. */
  220. public function get_item( $request ) {
  221. $tools = $this->get_tools();
  222. if ( empty( $tools[ $request['id'] ] ) ) {
  223. return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
  224. }
  225. $tool = $tools[ $request['id'] ];
  226. return rest_ensure_response(
  227. $this->prepare_item_for_response(
  228. array(
  229. 'id' => $request['id'],
  230. 'name' => $tool['name'],
  231. 'action' => $tool['button'],
  232. 'description' => $tool['desc'],
  233. ), $request
  234. )
  235. );
  236. }
  237. /**
  238. * Update (execute) a tool.
  239. *
  240. * @param WP_REST_Request $request Request data.
  241. * @return WP_Error|WP_REST_Response
  242. */
  243. public function update_item( $request ) {
  244. $tools = $this->get_tools();
  245. if ( empty( $tools[ $request['id'] ] ) ) {
  246. return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
  247. }
  248. $tool = $tools[ $request['id'] ];
  249. $tool = array(
  250. 'id' => $request['id'],
  251. 'name' => $tool['name'],
  252. 'action' => $tool['button'],
  253. 'description' => $tool['desc'],
  254. );
  255. $execute_return = $this->execute_tool( $request['id'] );
  256. $tool = array_merge( $tool, $execute_return );
  257. /**
  258. * Fires after a WooCommerce REST system status tool has been executed.
  259. *
  260. * @param array $tool Details about the tool that has been executed.
  261. * @param WP_REST_Request $request The current WP_REST_Request object.
  262. */
  263. do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request );
  264. $request->set_param( 'context', 'edit' );
  265. $response = $this->prepare_item_for_response( $tool, $request );
  266. return rest_ensure_response( $response );
  267. }
  268. /**
  269. * Prepare a tool item for serialization.
  270. *
  271. * @param array $item Object.
  272. * @param WP_REST_Request $request Request object.
  273. * @return WP_REST_Response $response Response data.
  274. */
  275. public function prepare_item_for_response( $item, $request ) {
  276. $context = empty( $request['context'] ) ? 'view' : $request['context'];
  277. $data = $this->add_additional_fields_to_object( $item, $request );
  278. $data = $this->filter_response_by_context( $data, $context );
  279. $response = rest_ensure_response( $data );
  280. $response->add_links( $this->prepare_links( $item['id'] ) );
  281. return $response;
  282. }
  283. /**
  284. * Get the system status tools schema, conforming to JSON Schema.
  285. *
  286. * @return array
  287. */
  288. public function get_item_schema() {
  289. $schema = array(
  290. '$schema' => 'http://json-schema.org/draft-04/schema#',
  291. 'title' => 'system_status_tool',
  292. 'type' => 'object',
  293. 'properties' => array(
  294. 'id' => array(
  295. 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ),
  296. 'type' => 'string',
  297. 'context' => array( 'view', 'edit' ),
  298. 'arg_options' => array(
  299. 'sanitize_callback' => 'sanitize_title',
  300. ),
  301. ),
  302. 'name' => array(
  303. 'description' => __( 'Tool name.', 'woocommerce' ),
  304. 'type' => 'string',
  305. 'context' => array( 'view', 'edit' ),
  306. 'arg_options' => array(
  307. 'sanitize_callback' => 'sanitize_text_field',
  308. ),
  309. ),
  310. 'action' => array(
  311. 'description' => __( 'What running the tool will do.', 'woocommerce' ),
  312. 'type' => 'string',
  313. 'context' => array( 'view', 'edit' ),
  314. 'arg_options' => array(
  315. 'sanitize_callback' => 'sanitize_text_field',
  316. ),
  317. ),
  318. 'description' => array(
  319. 'description' => __( 'Tool description.', 'woocommerce' ),
  320. 'type' => 'string',
  321. 'context' => array( 'view', 'edit' ),
  322. 'arg_options' => array(
  323. 'sanitize_callback' => 'sanitize_text_field',
  324. ),
  325. ),
  326. 'success' => array(
  327. 'description' => __( 'Did the tool run successfully?', 'woocommerce' ),
  328. 'type' => 'boolean',
  329. 'context' => array( 'edit' ),
  330. ),
  331. 'message' => array(
  332. 'description' => __( 'Tool return message.', 'woocommerce' ),
  333. 'type' => 'string',
  334. 'context' => array( 'edit' ),
  335. 'arg_options' => array(
  336. 'sanitize_callback' => 'sanitize_text_field',
  337. ),
  338. ),
  339. ),
  340. );
  341. return $this->add_additional_fields_schema( $schema );
  342. }
  343. /**
  344. * Prepare links for the request.
  345. *
  346. * @param string $id ID.
  347. * @return array
  348. */
  349. protected function prepare_links( $id ) {
  350. $base = '/' . $this->namespace . '/' . $this->rest_base;
  351. $links = array(
  352. 'item' => array(
  353. 'href' => rest_url( trailingslashit( $base ) . $id ),
  354. 'embeddable' => true,
  355. ),
  356. );
  357. return $links;
  358. }
  359. /**
  360. * Get any query params needed.
  361. *
  362. * @return array
  363. */
  364. public function get_collection_params() {
  365. return array(
  366. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  367. );
  368. }
  369. /**
  370. * Actually executes a tool.
  371. *
  372. * @param string $tool Tool.
  373. * @return array
  374. */
  375. public function execute_tool( $tool ) {
  376. global $wpdb;
  377. $ran = true;
  378. switch ( $tool ) {
  379. case 'clear_transients':
  380. wc_delete_product_transients();
  381. wc_delete_shop_order_transients();
  382. WC_Cache_Helper::get_transient_version( 'shipping', true );
  383. $message = __( 'Product transients cleared', 'woocommerce' );
  384. break;
  385. case 'clear_expired_transients':
  386. /* translators: %d: amount of expired transients */
  387. $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() );
  388. break;
  389. case 'delete_orphaned_variations':
  390. // Delete orphans.
  391. $result = absint(
  392. $wpdb->query(
  393. "DELETE products
  394. FROM {$wpdb->posts} products
  395. LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent
  396. WHERE wp.ID IS NULL AND products.post_type = 'product_variation';"
  397. )
  398. );
  399. /* translators: %d: amount of orphaned variations */
  400. $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result );
  401. break;
  402. case 'clear_expired_download_permissions':
  403. // Delete expired download permissions and ones with 0 downloads remaining.
  404. $result = absint(
  405. $wpdb->query(
  406. $wpdb->prepare(
  407. "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
  408. WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )",
  409. date( 'Y-m-d', current_time( 'timestamp' ) )
  410. )
  411. )
  412. );
  413. /* translators: %d: amount of permissions */
  414. $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result );
  415. break;
  416. case 'add_order_indexes':
  417. /*
  418. * Add billing and shipping address indexes containing the customer name for orders
  419. * that don't have address indexes yet.
  420. */
  421. $sql = "INSERT INTO {$wpdb->postmeta}( post_id, meta_key, meta_value )
  422. SELECT post_id, '%s', GROUP_CONCAT( meta_value SEPARATOR ' ' )
  423. FROM {$wpdb->postmeta}
  424. WHERE meta_key IN ( '%s', '%s' )
  425. AND post_id IN ( SELECT DISTINCT post_id FROM {$wpdb->postmeta}
  426. WHERE post_id NOT IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='%s' )
  427. AND post_id IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='%s' ) )
  428. GROUP BY post_id";
  429. $rows = $wpdb->query( $wpdb->prepare( $sql, '_billing_address_index', '_billing_first_name', '_billing_last_name', '_billing_address_index', '_billing_last_name' ) ); // WPCS: unprepared SQL ok.
  430. $rows += $wpdb->query( $wpdb->prepare( $sql, '_shipping_address_index', '_shipping_first_name', '_shipping_last_name', '_shipping_address_index', '_shipping_last_name' ) ); // WPCS: unprepared SQL ok.
  431. /* translators: %d: amount of indexes */
  432. $message = sprintf( __( '%d indexes added', 'woocommerce' ), $rows );
  433. break;
  434. case 'reset_roles':
  435. // Remove then re-add caps and roles.
  436. WC_Install::remove_roles();
  437. WC_Install::create_roles();
  438. $message = __( 'Roles successfully reset', 'woocommerce' );
  439. break;
  440. case 'recount_terms':
  441. $product_cats = get_terms(
  442. 'product_cat', array(
  443. 'hide_empty' => false,
  444. 'fields' => 'id=>parent',
  445. )
  446. );
  447. _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false );
  448. $product_tags = get_terms(
  449. 'product_tag', array(
  450. 'hide_empty' => false,
  451. 'fields' => 'id=>parent',
  452. )
  453. );
  454. _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false );
  455. $message = __( 'Terms successfully recounted', 'woocommerce' );
  456. break;
  457. case 'clear_sessions':
  458. $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" );
  459. $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok.
  460. wp_cache_flush();
  461. /* translators: %d: amount of sessions */
  462. $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) );
  463. break;
  464. case 'install_pages':
  465. WC_Install::create_pages();
  466. $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' );
  467. break;
  468. case 'delete_taxes':
  469. $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" );
  470. $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" );
  471. WC_Cache_Helper::incr_cache_prefix( 'taxes' );
  472. $message = __( 'Tax rates successfully deleted', 'woocommerce' );
  473. break;
  474. case 'reset_tracking':
  475. if ( ! class_exists( 'WC_Tracker' ) ) {
  476. include_once WC_ABSPATH . 'includes/class-wc-tracker.php';
  477. }
  478. WC_Tracker::opt_out_request();
  479. delete_option( 'woocommerce_allow_tracking' );
  480. WC_Admin_Notices::add_notice( 'tracking' );
  481. $message = __( 'Usage tracking settings successfully reset.', 'woocommerce' );
  482. break;
  483. case 'regenerate_thumbnails':
  484. WC_Regenerate_Images::queue_image_regeneration();
  485. $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' );
  486. break;
  487. default:
  488. $tools = $this->get_tools();
  489. if ( isset( $tools[ $tool ]['callback'] ) ) {
  490. $callback = $tools[ $tool ]['callback'];
  491. $return = call_user_func( $callback );
  492. if ( is_string( $return ) ) {
  493. $message = $return;
  494. } elseif ( false === $return ) {
  495. $callback_string = is_array( $callback ) ? get_class( $callback[0] ) . '::' . $callback[1] : $callback;
  496. $ran = false;
  497. /* translators: %s: callback string */
  498. $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string );
  499. } else {
  500. $message = __( 'Tool ran.', 'woocommerce' );
  501. }
  502. } else {
  503. $ran = false;
  504. $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' );
  505. }
  506. break;
  507. }
  508. return array(
  509. 'success' => $ran,
  510. 'message' => $message,
  511. );
  512. }
  513. }