/includes/api/v1/class-wc-rest-webhooks-controller.php

  1. <?php 
  2. /** 
  3. * REST API Webhooks controller 
  4. * 
  5. * Handles requests to the /webhooks endpoint. 
  6. * 
  7. * @author WooThemes 
  8. * @category API 
  9. * @package WooCommerce/API 
  10. * @since 3.0.0 
  11. */ 
  12.  
  13. if ( ! defined( 'ABSPATH' ) ) { 
  14. exit; 
  15.  
  16. /** 
  17. * REST API Webhooks controller class. 
  18. * 
  19. * @package WooCommerce/API 
  20. * @extends WC_REST_Posts_Controller 
  21. */ 
  22. class WC_REST_Webhooks_V1_Controller extends WC_REST_Posts_Controller { 
  23.  
  24. /** 
  25. * Endpoint namespace. 
  26. * 
  27. * @var string 
  28. */ 
  29. protected $namespace = 'wc/v1'; 
  30.  
  31. /** 
  32. * Route base. 
  33. * 
  34. * @var string 
  35. */ 
  36. protected $rest_base = 'webhooks'; 
  37.  
  38. /** 
  39. * Post type. 
  40. * 
  41. * @var string 
  42. */ 
  43. protected $post_type = 'shop_webhook'; 
  44.  
  45. /** 
  46. * Initialize Webhooks actions. 
  47. */ 
  48. public function __construct() { 
  49. add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); 
  50.  
  51. /** 
  52. * Register the routes for webhooks. 
  53. */ 
  54. public function register_routes() { 
  55. register_rest_route( $this->namespace, '/' . $this->rest_base, array( 
  56. array( 
  57. 'methods' => WP_REST_Server::READABLE,  
  58. 'callback' => array( $this, 'get_items' ),  
  59. 'permission_callback' => array( $this, 'get_items_permissions_check' ),  
  60. 'args' => $this->get_collection_params(),  
  61. ),  
  62. array( 
  63. 'methods' => WP_REST_Server::CREATABLE,  
  64. 'callback' => array( $this, 'create_item' ),  
  65. 'permission_callback' => array( $this, 'create_item_permissions_check' ),  
  66. 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 
  67. 'topic' => array( 
  68. 'required' => true,  
  69. 'type' => 'string',  
  70. 'description' => __( 'Webhook topic.', 'woocommerce' ),  
  71. ),  
  72. 'delivery_url' => array( 
  73. 'required' => true,  
  74. 'type' => 'string',  
  75. 'description' => __( 'Webhook delivery URL.', 'woocommerce' ),  
  76. ),  
  77. 'secret' => array( 
  78. 'required' => true,  
  79. 'type' => 'string',  
  80. 'description' => __( 'Webhook secret.', 'woocommerce' ),  
  81. ),  
  82. ) ),  
  83. ),  
  84. 'schema' => array( $this, 'get_public_item_schema' ),  
  85. ) ); 
  86.  
  87. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 
  88. 'args' => array( 
  89. 'id' => array( 
  90. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),  
  91. 'type' => 'integer',  
  92. ),  
  93. ),  
  94. array( 
  95. 'methods' => WP_REST_Server::READABLE,  
  96. 'callback' => array( $this, 'get_item' ),  
  97. 'permission_callback' => array( $this, 'get_item_permissions_check' ),  
  98. 'args' => array( 
  99. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),  
  100. ),  
  101. ),  
  102. array( 
  103. 'methods' => WP_REST_Server::EDITABLE,  
  104. 'callback' => array( $this, 'update_item' ),  
  105. 'permission_callback' => array( $this, 'update_item_permissions_check' ),  
  106. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),  
  107. ),  
  108. array( 
  109. 'methods' => WP_REST_Server::DELETABLE,  
  110. 'callback' => array( $this, 'delete_item' ),  
  111. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),  
  112. 'args' => array( 
  113. 'force' => array( 
  114. 'default' => false,  
  115. 'type' => 'boolean',  
  116. 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),  
  117. ),  
  118. ),  
  119. ),  
  120. 'schema' => array( $this, 'get_public_item_schema' ),  
  121. ) ); 
  122.  
  123. register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 
  124. array( 
  125. 'methods' => WP_REST_Server::EDITABLE,  
  126. 'callback' => array( $this, 'batch_items' ),  
  127. 'permission_callback' => array( $this, 'batch_items_permissions_check' ),  
  128. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),  
  129. ),  
  130. 'schema' => array( $this, 'get_public_batch_schema' ),  
  131. ) ); 
  132.  
  133. /** 
  134. * Get the default REST API version. 
  135. * 
  136. * @since 3.0.0 
  137. * @return string 
  138. */ 
  139. protected function get_default_api_version() { 
  140. return 'wp_api_v2'; 
  141.  
  142. /** 
  143. * Create a single webhook. 
  144. * 
  145. * @param WP_REST_Request $request Full details about the request. 
  146. * @return WP_Error|WP_REST_Response 
  147. */ 
  148. public function create_item( $request ) { 
  149. if ( ! empty( $request['id'] ) ) { 
  150. /** translators: %s: post type */ 
  151. return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); 
  152.  
  153. // Validate topic. 
  154. if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { 
  155. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); 
  156.  
  157. // Validate delivery URL. 
  158. if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { 
  159. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); 
  160.  
  161. $post = $this->prepare_item_for_database( $request ); 
  162. if ( is_wp_error( $post ) ) { 
  163. return $post; 
  164.  
  165. $post->post_type = $this->post_type; 
  166. $post_id = wp_insert_post( $post, true ); 
  167.  
  168. if ( is_wp_error( $post_id ) ) { 
  169.  
  170. if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { 
  171. $post_id->add_data( array( 'status' => 500 ) ); 
  172. } else { 
  173. $post_id->add_data( array( 'status' => 400 ) ); 
  174. return $post_id; 
  175. $post->ID = $post_id; 
  176.  
  177. $webhook = new WC_Webhook( $post_id ); 
  178.  
  179. // Set topic. 
  180. $webhook->set_topic( $request['topic'] ); 
  181.  
  182. // Set delivery URL. 
  183. $webhook->set_delivery_url( $request['delivery_url'] ); 
  184.  
  185. // Set secret. 
  186. $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : '' ); 
  187.  
  188. // Set API version to WP API integration. 
  189. $webhook->set_api_version( $this->get_default_api_version() ); 
  190.  
  191. // Set status. 
  192. if ( ! empty( $request['status'] ) ) { 
  193. $webhook->update_status( $request['status'] ); 
  194.  
  195. $post = get_post( $post_id ); 
  196. $this->update_additional_fields_for_object( $post, $request ); 
  197.  
  198. /** 
  199. * Fires after a single item is created or updated via the REST API. 
  200. * 
  201. * @param WP_Post $post Inserted object. 
  202. * @param WP_REST_Request $request Request object. 
  203. * @param boolean $creating True when creating item, false when updating. 
  204. */ 
  205. do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); 
  206.  
  207. $request->set_param( 'context', 'edit' ); 
  208. $response = $this->prepare_item_for_response( $post, $request ); 
  209. $response = rest_ensure_response( $response ); 
  210. $response->set_status( 201 ); 
  211. $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); 
  212.  
  213. // Send ping. 
  214. $webhook->deliver_ping(); 
  215.  
  216. // Clear cache. 
  217. delete_transient( 'woocommerce_webhook_ids' ); 
  218.  
  219. return $response; 
  220.  
  221. /** 
  222. * Update a single webhook. 
  223. * 
  224. * @param WP_REST_Request $request Full details about the request. 
  225. * @return WP_Error|WP_REST_Response 
  226. */ 
  227. public function update_item( $request ) { 
  228. $id = (int) $request['id']; 
  229. $post = get_post( $id ); 
  230.  
  231. if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { 
  232. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); 
  233.  
  234. $webhook = new WC_Webhook( $id ); 
  235.  
  236. // Update topic. 
  237. if ( ! empty( $request['topic'] ) ) { 
  238. if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { 
  239. $webhook->set_topic( $request['topic'] ); 
  240. } else { 
  241. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); 
  242.  
  243. // Update delivery URL. 
  244. if ( ! empty( $request['delivery_url'] ) ) { 
  245. if ( wc_is_valid_url( $request['delivery_url'] ) ) { 
  246. $webhook->set_delivery_url( $request['delivery_url'] ); 
  247. } else { 
  248. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); 
  249.  
  250. // Update secret. 
  251. if ( ! empty( $request['secret'] ) ) { 
  252. $webhook->set_secret( $request['secret'] ); 
  253.  
  254. // Update status. 
  255. if ( ! empty( $request['status'] ) ) { 
  256. $webhook->update_status( $request['status'] ); 
  257.  
  258. $post = $this->prepare_item_for_database( $request ); 
  259. if ( is_wp_error( $post ) ) { 
  260. return $post; 
  261.  
  262. // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. 
  263. $post_id = wp_update_post( (array) $post, true ); 
  264. if ( is_wp_error( $post_id ) ) { 
  265. if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { 
  266. $post_id->add_data( array( 'status' => 500 ) ); 
  267. } else { 
  268. $post_id->add_data( array( 'status' => 400 ) ); 
  269. return $post_id; 
  270.  
  271. $post = get_post( $post_id ); 
  272. $this->update_additional_fields_for_object( $post, $request ); 
  273.  
  274. /** 
  275. * Fires after a single item is created or updated via the REST API. 
  276. * 
  277. * @param WP_Post $post Inserted object. 
  278. * @param WP_REST_Request $request Request object. 
  279. * @param boolean $creating True when creating item, false when updating. 
  280. */ 
  281. do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); 
  282.  
  283. $request->set_param( 'context', 'edit' ); 
  284. $response = $this->prepare_item_for_response( $post, $request ); 
  285.  
  286. // Clear cache. 
  287. delete_transient( 'woocommerce_webhook_ids' ); 
  288.  
  289. return rest_ensure_response( $response ); 
  290.  
  291. /** 
  292. * Delete a single webhook. 
  293. * 
  294. * @param WP_REST_Request $request Full details about the request. 
  295. * @return WP_REST_Response|WP_Error 
  296. */ 
  297. public function delete_item( $request ) { 
  298. $id = (int) $request['id']; 
  299. $force = isset( $request['force'] ) ? (bool) $request['force'] : false; 
  300.  
  301. // We don't support trashing for this type, error out. 
  302. if ( ! $force ) { 
  303. return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); 
  304.  
  305. $post = get_post( $id ); 
  306.  
  307. if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { 
  308. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); 
  309.  
  310. $request->set_param( 'context', 'edit' ); 
  311. $response = $this->prepare_item_for_response( $post, $request ); 
  312.  
  313. $result = wp_delete_post( $id, true ); 
  314.  
  315. if ( ! $result ) { 
  316. /** translators: %s: post type */ 
  317. return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); 
  318.  
  319. /** 
  320. * Fires after a single item is deleted or trashed via the REST API. 
  321. * 
  322. * @param object $post The deleted or trashed item. 
  323. * @param WP_REST_Response $response The response data. 
  324. * @param WP_REST_Request $request The request sent to the API. 
  325. */ 
  326. do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); 
  327.  
  328. // Clear cache. 
  329. delete_transient( 'woocommerce_webhook_ids' ); 
  330.  
  331. return $response; 
  332.  
  333. /** 
  334. * Prepare a single webhook for create or update. 
  335. * 
  336. * @param WP_REST_Request $request Request object. 
  337. * @return WP_Error|stdClass $data Post object. 
  338. */ 
  339. protected function prepare_item_for_database( $request ) { 
  340. global $wpdb; 
  341.  
  342. $data = new stdClass; 
  343.  
  344. // Post ID. 
  345. if ( isset( $request['id'] ) ) { 
  346. $data->ID = absint( $request['id'] ); 
  347.  
  348. // Validate required POST fields. 
  349. if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { 
  350. // @codingStandardsIgnoreStart 
  351. $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); 
  352. // @codingStandardsIgnoreEnd 
  353.  
  354. // Post author. 
  355. $data->post_author = get_current_user_id(); 
  356.  
  357. // Post password. 
  358. $password = strlen( uniqid( 'webhook_' ) ); 
  359. $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $password; 
  360.  
  361. // Post status. 
  362. $data->post_status = 'publish'; 
  363. } else { 
  364.  
  365. // Allow edit post title. 
  366. if ( ! empty( $request['name'] ) ) { 
  367. $data->post_title = $request['name']; 
  368.  
  369. // Comment status. 
  370. $data->comment_status = 'closed'; 
  371.  
  372. // Ping status. 
  373. $data->ping_status = 'closed'; 
  374.  
  375. /** 
  376. * Filter the query_vars used in `get_items` for the constructed query. 
  377. * 
  378. * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being 
  379. * prepared for insertion. 
  380. * 
  381. * @param stdClass $data An object representing a single item prepared 
  382. * for inserting or updating the database. 
  383. * @param WP_REST_Request $request Request object. 
  384. */ 
  385. return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); 
  386.  
  387. /** 
  388. * Prepare a single webhook output for response. 
  389. * 
  390. * @param WP_REST_Request $request Request object. 
  391. * @return WP_REST_Response $response Response data. 
  392. */ 
  393. public function prepare_item_for_response( $post, $request ) { 
  394. $id = (int) $post->ID; 
  395. $webhook = new WC_Webhook( $id ); 
  396. $data = array( 
  397. 'id' => $webhook->id,  
  398. 'name' => $webhook->get_name(),  
  399. 'status' => $webhook->get_status(),  
  400. 'topic' => $webhook->get_topic(),  
  401. 'resource' => $webhook->get_resource(),  
  402. 'event' => $webhook->get_event(),  
  403. 'hooks' => $webhook->get_hooks(),  
  404. 'delivery_url' => $webhook->get_delivery_url(),  
  405. 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ),  
  406. 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ),  
  407. ); 
  408.  
  409. $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 
  410. $data = $this->add_additional_fields_to_object( $data, $request ); 
  411. $data = $this->filter_response_by_context( $data, $context ); 
  412.  
  413. // Wrap the data in a response object. 
  414. $response = rest_ensure_response( $data ); 
  415.  
  416. $response->add_links( $this->prepare_links( $post, $request ) ); 
  417.  
  418. /** 
  419. * Filter webhook object returned from the REST API. 
  420. * 
  421. * @param WP_REST_Response $response The response object. 
  422. * @param WC_Webhook $webhook Webhook object used to create response. 
  423. * @param WP_REST_Request $request Request object. 
  424. */ 
  425. return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); 
  426.  
  427. /** 
  428. * Query args. 
  429. * 
  430. * @param array $args 
  431. * @param WP_REST_Request $request 
  432. * @return array 
  433. */ 
  434. public function query_args( $args, $request ) { 
  435. // Set post_status. 
  436. switch ( $request['status'] ) { 
  437. case 'active' : 
  438. $args['post_status'] = 'publish'; 
  439. break; 
  440. case 'paused' : 
  441. $args['post_status'] = 'draft'; 
  442. break; 
  443. case 'disabled' : 
  444. $args['post_status'] = 'pending'; 
  445. break; 
  446. default : 
  447. $args['post_status'] = 'any'; 
  448. break; 
  449.  
  450. return $args; 
  451.  
  452. /** 
  453. * Get the Webhook's schema, conforming to JSON Schema. 
  454. * 
  455. * @return array 
  456. */ 
  457. public function get_item_schema() { 
  458. $schema = array( 
  459. '$schema' => 'http://json-schema.org/draft-04/schema#',  
  460. 'title' => 'webhook',  
  461. 'type' => 'object',  
  462. 'properties' => array( 
  463. 'id' => array( 
  464. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),  
  465. 'type' => 'integer',  
  466. 'context' => array( 'view', 'edit' ),  
  467. 'readonly' => true,  
  468. ),  
  469. 'name' => array( 
  470. 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ),  
  471. 'type' => 'string',  
  472. 'context' => array( 'view', 'edit' ),  
  473. ),  
  474. 'status' => array( 
  475. 'description' => __( 'Webhook status.', 'woocommerce' ),  
  476. 'type' => 'string',  
  477. 'default' => 'active',  
  478. 'enum' => array( 'active', 'paused', 'disabled' ),  
  479. 'context' => array( 'view', 'edit' ),  
  480. 'arg_options' => array( 
  481. 'sanitize_callback' => 'wc_is_webhook_valid_topic',  
  482. ),  
  483. ),  
  484. 'topic' => array( 
  485. 'description' => __( 'Webhook topic.', 'woocommerce' ),  
  486. 'type' => 'string',  
  487. 'context' => array( 'view', 'edit' ),  
  488. ),  
  489. 'resource' => array( 
  490. 'description' => __( 'Webhook resource.', 'woocommerce' ),  
  491. 'type' => 'string',  
  492. 'context' => array( 'view', 'edit' ),  
  493. 'readonly' => true,  
  494. ),  
  495. 'event' => array( 
  496. 'description' => __( 'Webhook event.', 'woocommerce' ),  
  497. 'type' => 'string',  
  498. 'context' => array( 'view', 'edit' ),  
  499. 'readonly' => true,  
  500. ),  
  501. 'hooks' => array( 
  502. 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ),  
  503. 'type' => 'array',  
  504. 'context' => array( 'view', 'edit' ),  
  505. 'readonly' => true,  
  506. 'items' => array( 
  507. 'type' => 'string',  
  508. ),  
  509. ),  
  510. 'delivery_url' => array( 
  511. 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ),  
  512. 'type' => 'string',  
  513. 'format' => 'uri',  
  514. 'context' => array( 'view', 'edit' ),  
  515. 'readonly' => true,  
  516. ),  
  517. 'secret' => array( 
  518. 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default is a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ),  
  519. 'type' => 'string',  
  520. 'context' => array( 'edit' ),  
  521. ),  
  522. 'date_created' => array( 
  523. 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ),  
  524. 'type' => 'date-time',  
  525. 'context' => array( 'view', 'edit' ),  
  526. 'readonly' => true,  
  527. ),  
  528. 'date_modified' => array( 
  529. 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ),  
  530. 'type' => 'date-time',  
  531. 'context' => array( 'view', 'edit' ),  
  532. 'readonly' => true,  
  533. ),  
  534. ),  
  535. ); 
  536.  
  537. return $this->add_additional_fields_schema( $schema ); 
  538.  
  539. /** 
  540. * Get the query params for collections of attachments. 
  541. * 
  542. * @return array 
  543. */ 
  544. public function get_collection_params() { 
  545. $params = parent::get_collection_params(); 
  546.  
  547. $params['status'] = array( 
  548. 'default' => 'all',  
  549. 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ),  
  550. 'type' => 'string',  
  551. 'enum' => array( 'all', 'active', 'paused', 'disabled' ),  
  552. 'sanitize_callback' => 'sanitize_key',  
  553. 'validate_callback' => 'rest_validate_request_arg',  
  554. ); 
  555.  
  556. return $params; 
.