Нет описания

class-wc-webhook-data-store.php 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <?php
  2. /**
  3. * Webhook Data Store
  4. *
  5. * @version 3.3.0
  6. * @package WooCommerce\Classes\Data_Store
  7. */
  8. if ( ! defined( 'ABSPATH' ) ) {
  9. exit;
  10. }
  11. /**
  12. * Webhook data store class.
  13. */
  14. class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
  15. /**
  16. * Create a new webhook in the database.
  17. *
  18. * @since 3.3.0
  19. * @param WC_Webhook $webhook Webhook instance.
  20. */
  21. public function create( &$webhook ) {
  22. global $wpdb;
  23. $changes = $webhook->get_changes();
  24. if ( isset( $changes['date_created'] ) ) {
  25. $date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' );
  26. $date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() );
  27. } else {
  28. $date_created = current_time( 'mysql' );
  29. $date_created_gmt = current_time( 'mysql', 1 );
  30. $webhook->set_date_created( $date_created );
  31. }
  32. // Pending delivery by default if not set while creating a new webhook.
  33. if ( ! isset( $changes['pending_delivery'] ) ) {
  34. $webhook->set_pending_delivery( true );
  35. }
  36. $data = array(
  37. 'status' => $webhook->get_status( 'edit' ),
  38. 'name' => $webhook->get_name( 'edit' ),
  39. 'user_id' => $webhook->get_user_id( 'edit' ),
  40. 'delivery_url' => $webhook->get_delivery_url( 'edit' ),
  41. 'secret' => $webhook->get_secret( 'edit' ),
  42. 'topic' => $webhook->get_topic( 'edit' ),
  43. 'date_created' => $date_created,
  44. 'date_created_gmt' => $date_created_gmt,
  45. 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
  46. 'failure_count' => $webhook->get_failure_count( 'edit' ),
  47. 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
  48. );
  49. $wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok.
  50. $webhook_id = $wpdb->insert_id;
  51. $webhook->set_id( $webhook_id );
  52. $webhook->apply_changes();
  53. $this->delete_transients( $webhook->get_status( 'edit' ) );
  54. WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
  55. do_action( 'woocommerce_new_webhook', $webhook_id, $webhook );
  56. }
  57. /**
  58. * Read a webhook from the database.
  59. *
  60. * @since 3.3.0
  61. * @param WC_Webhook $webhook Webhook instance.
  62. * @throws Exception When webhook is invalid.
  63. */
  64. public function read( &$webhook ) {
  65. global $wpdb;
  66. $data = wp_cache_get( $webhook->get_id(), 'webhooks' );
  67. if ( false === $data ) {
  68. $data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok.
  69. wp_cache_add( $webhook->get_id(), $data, 'webhooks' );
  70. }
  71. if ( is_array( $data ) ) {
  72. $webhook->set_props(
  73. array(
  74. 'id' => $data['webhook_id'],
  75. 'status' => $data['status'],
  76. 'name' => $data['name'],
  77. 'user_id' => $data['user_id'],
  78. 'delivery_url' => $data['delivery_url'],
  79. 'secret' => $data['secret'],
  80. 'topic' => $data['topic'],
  81. 'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'],
  82. 'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'],
  83. 'api_version' => $data['api_version'],
  84. 'failure_count' => $data['failure_count'],
  85. 'pending_delivery' => $data['pending_delivery'],
  86. )
  87. );
  88. $webhook->set_object_read( true );
  89. do_action( 'woocommerce_webhook_loaded', $webhook );
  90. } else {
  91. throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) );
  92. }
  93. }
  94. /**
  95. * Update a webhook.
  96. *
  97. * @since 3.3.0
  98. * @param WC_Webhook $webhook Webhook instance.
  99. */
  100. public function update( &$webhook ) {
  101. global $wpdb;
  102. $changes = $webhook->get_changes();
  103. $trigger = isset( $changes['delivery_url'] );
  104. if ( isset( $changes['date_modified'] ) ) {
  105. $date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' );
  106. $date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() );
  107. } else {
  108. $date_modified = current_time( 'mysql' );
  109. $date_modified_gmt = current_time( 'mysql', 1 );
  110. $webhook->set_date_modified( $date_modified );
  111. }
  112. $data = array(
  113. 'status' => $webhook->get_status( 'edit' ),
  114. 'name' => $webhook->get_name( 'edit' ),
  115. 'user_id' => $webhook->get_user_id( 'edit' ),
  116. 'delivery_url' => $webhook->get_delivery_url( 'edit' ),
  117. 'secret' => $webhook->get_secret( 'edit' ),
  118. 'topic' => $webhook->get_topic( 'edit' ),
  119. 'date_modified' => $date_modified,
  120. 'date_modified_gmt' => $date_modified_gmt,
  121. 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
  122. 'failure_count' => $webhook->get_failure_count( 'edit' ),
  123. 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
  124. );
  125. $wpdb->update(
  126. $wpdb->prefix . 'wc_webhooks',
  127. $data,
  128. array(
  129. 'webhook_id' => $webhook->get_id(),
  130. )
  131. ); // WPCS: DB call ok.
  132. $webhook->apply_changes();
  133. if ( isset( $changes['status'] ) ) {
  134. // We need to delete all transients, because we can't be sure of the old status.
  135. $this->delete_transients( 'all' );
  136. }
  137. wp_cache_delete( $webhook->get_id(), 'webhooks' );
  138. WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
  139. if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) {
  140. $webhook->deliver_ping();
  141. }
  142. do_action( 'woocommerce_webhook_updated', $webhook->get_id() );
  143. }
  144. /**
  145. * Remove a webhook from the database.
  146. *
  147. * @since 3.3.0
  148. * @param WC_Webhook $webhook Webhook instance.
  149. */
  150. public function delete( &$webhook ) {
  151. global $wpdb;
  152. $wpdb->delete(
  153. $wpdb->prefix . 'wc_webhooks',
  154. array(
  155. 'webhook_id' => $webhook->get_id(),
  156. ),
  157. array( '%d' )
  158. ); // WPCS: cache ok, DB call ok.
  159. $this->delete_transients( 'all' );
  160. wp_cache_delete( $webhook->get_id(), 'webhooks' );
  161. WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
  162. do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook );
  163. }
  164. /**
  165. * Get API version number.
  166. *
  167. * @since 3.3.0
  168. * @param string $api_version REST API version.
  169. * @return int
  170. */
  171. public function get_api_version_number( $api_version ) {
  172. return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) );
  173. }
  174. /**
  175. * Get webhooks IDs from the database.
  176. *
  177. * @since 3.3.0
  178. * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys.
  179. * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0.
  180. * @return int[]
  181. */
  182. public function get_webhooks_ids( $status = '' ) {
  183. if ( ! empty( $status ) ) {
  184. $this->validate_status( $status );
  185. }
  186. $ids = get_transient( $this->get_transient_key( $status ) );
  187. if ( false === $ids ) {
  188. $ids = $this->search_webhooks(
  189. array(
  190. 'limit' => -1,
  191. 'status' => $status,
  192. )
  193. );
  194. $ids = array_map( 'absint', $ids );
  195. set_transient( $this->get_transient_key( $status ), $ids );
  196. }
  197. return $ids;
  198. }
  199. /**
  200. * Search webhooks.
  201. *
  202. * @param array $args Search arguments.
  203. * @return array|object
  204. */
  205. public function search_webhooks( $args ) {
  206. global $wpdb;
  207. $args = wp_parse_args(
  208. $args,
  209. array(
  210. 'limit' => 10,
  211. 'offset' => 0,
  212. 'order' => 'DESC',
  213. 'orderby' => 'id',
  214. 'paginate' => false,
  215. )
  216. );
  217. // Map post statuses.
  218. $statuses = array(
  219. 'publish' => 'active',
  220. 'draft' => 'paused',
  221. 'pending' => 'disabled',
  222. );
  223. // Map orderby to support a few post keys.
  224. $orderby_mapping = array(
  225. 'ID' => 'webhook_id',
  226. 'id' => 'webhook_id',
  227. 'name' => 'name',
  228. 'title' => 'name',
  229. 'post_title' => 'name',
  230. 'post_name' => 'name',
  231. 'date_created' => 'date_created_gmt',
  232. 'date' => 'date_created_gmt',
  233. 'post_date' => 'date_created_gmt',
  234. 'date_modified' => 'date_modified_gmt',
  235. 'modified' => 'date_modified_gmt',
  236. 'post_modified' => 'date_modified_gmt',
  237. );
  238. $orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id';
  239. $sort = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
  240. $order = "ORDER BY {$orderby} {$sort}";
  241. $limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
  242. $offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
  243. $status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : '';
  244. $search = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : '';
  245. $include = '';
  246. $exclude = '';
  247. $date_created = '';
  248. $date_modified = '';
  249. if ( ! empty( $args['include'] ) ) {
  250. $args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) );
  251. $include = 'AND webhook_id IN (' . $args['include'] . ')';
  252. }
  253. if ( ! empty( $args['exclude'] ) ) {
  254. $args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) );
  255. $exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')';
  256. }
  257. if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
  258. $args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
  259. $args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];
  260. $date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
  261. }
  262. if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
  263. $args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
  264. $args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];
  265. $date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
  266. }
  267. // Check for cache.
  268. $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) );
  269. $cache_value = wp_cache_get( $cache_key, 'webhook_search_results' );
  270. if ( $cache_value ) {
  271. return $cache_value;
  272. }
  273. if ( $args['paginate'] ) {
  274. $query = trim(
  275. "SELECT SQL_CALC_FOUND_ROWS webhook_id
  276. FROM {$wpdb->prefix}wc_webhooks
  277. WHERE 1=1
  278. {$status}
  279. {$search}
  280. {$include}
  281. {$exclude}
  282. {$date_created}
  283. {$date_modified}
  284. {$order}
  285. {$limit}
  286. {$offset}"
  287. );
  288. $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  289. $total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' );
  290. $return_value = (object) array(
  291. 'webhooks' => $webhook_ids,
  292. 'total' => $total,
  293. 'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1,
  294. );
  295. } else {
  296. $query = trim(
  297. "SELECT webhook_id
  298. FROM {$wpdb->prefix}wc_webhooks
  299. WHERE 1=1
  300. {$status}
  301. {$search}
  302. {$include}
  303. {$exclude}
  304. {$date_created}
  305. {$date_modified}
  306. {$order}
  307. {$limit}
  308. {$offset}"
  309. );
  310. $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  311. $return_value = $webhook_ids;
  312. }
  313. wp_cache_set( $cache_key, $return_value, 'webhook_search_results' );
  314. return $return_value;
  315. }
  316. /**
  317. * Count webhooks.
  318. *
  319. * @since 3.6.0
  320. * @param string $status Status to count.
  321. * @return int
  322. */
  323. protected function get_webhook_count( $status = 'active' ) {
  324. global $wpdb;
  325. $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count';
  326. $count = wp_cache_get( $cache_key, 'webhooks' );
  327. if ( false === $count ) {
  328. $count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) );
  329. wp_cache_add( $cache_key, $count, 'webhooks' );
  330. }
  331. return $count;
  332. }
  333. /**
  334. * Get total webhook counts by status.
  335. *
  336. * @return array
  337. */
  338. public function get_count_webhooks_by_status() {
  339. $statuses = array_keys( wc_get_webhook_statuses() );
  340. $counts = array();
  341. foreach ( $statuses as $status ) {
  342. $counts[ $status ] = $this->get_webhook_count( $status );
  343. }
  344. return $counts;
  345. }
  346. /**
  347. * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses().
  348. *
  349. * @since 3.6.0
  350. * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys.
  351. * @param string $status Status to check.
  352. */
  353. private function validate_status( $status ) {
  354. if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
  355. throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) );
  356. }
  357. }
  358. /**
  359. * Get the transient key used to cache a set of webhook IDs, optionally filtered by status.
  360. *
  361. * @since 3.6.0
  362. * @param string $status Optional - status of cache key.
  363. * @return string
  364. */
  365. private function get_transient_key( $status = '' ) {
  366. return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status );
  367. }
  368. /**
  369. * Delete the transients used to cache a set of webhook IDs, optionally filtered by status.
  370. *
  371. * @since 3.6.0
  372. * @param string $status Optional - status of cache to delete, or 'all' to delete all caches.
  373. */
  374. private function delete_transients( $status = '' ) {
  375. // Always delete the non-filtered cache.
  376. delete_transient( $this->get_transient_key( '' ) );
  377. if ( ! empty( $status ) ) {
  378. if ( 'all' === $status ) {
  379. foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) {
  380. delete_transient( $this->get_transient_key( $status_key ) );
  381. }
  382. } else {
  383. delete_transient( $this->get_transient_key( $status ) );
  384. }
  385. }
  386. }
  387. }