PluginProbe
WANotifier for Forms and Actions / 2.7.5
WANotifier for Forms and Actions v2.7.5
3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 All 67 releases
notifier / libraries / action-scheduler / classes / data-stores / ActionScheduler_wpPostStore.php

ActionScheduler_wpPostStore.php in WANotifier for Forms and Actions 2.7.5, at libraries/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php

1,089 lines 34.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class ActionScheduler_wpPostStore
5 */
6 class ActionScheduler_wpPostStore extends ActionScheduler_Store {
7 const POST_TYPE = 'scheduled-action';
8 const GROUP_TAXONOMY = 'action-group';
9 const SCHEDULE_META_KEY = '_action_manager_schedule';
10 const DEPENDENCIES_MET = 'as-post-store-dependencies-met';
11
12 /**
13 * Used to share information about the before_date property of claims internally.
14 *
15 * This is used in preference to passing the same information as a method param
16 * for backwards-compatibility reasons.
17 *
18 * @var DateTime|null
19 */
20 private $claim_before_date = null;
21
22 /**
23 * Local Timezone.
24 *
25 * @var DateTimeZone
26 */
27 protected $local_timezone = null;
28
29 /**
30 * Save action.
31 *
32 * @param ActionScheduler_Action $action Scheduled Action.
33 * @param DateTime $scheduled_date Scheduled Date.
34 *
35 * @throws RuntimeException Throws an exception if the action could not be saved.
36 * @return int
37 */
38 public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = null ) {
39 try {
40 $this->validate_action( $action );
41 $post_array = $this->create_post_array( $action, $scheduled_date );
42 $post_id = $this->save_post_array( $post_array );
43 $this->save_post_schedule( $post_id, $action->get_schedule() );
44 $this->save_action_group( $post_id, $action->get_group() );
45 do_action( 'action_scheduler_stored_action', $post_id );
46 return $post_id;
47 } catch ( Exception $e ) {
48 /* translators: %s: action error message */
49 throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
50 }
51 }
52
53 /**
54 * Create post array.
55 *
56 * @param ActionScheduler_Action $action Scheduled Action.
57 * @param DateTime $scheduled_date Scheduled Date.
58 *
59 * @return array Returns an array of post data.
60 */
61 protected function create_post_array( ActionScheduler_Action $action, DateTime $scheduled_date = null ) {
62 $post = array(
63 'post_type' => self::POST_TYPE,
64 'post_title' => $action->get_hook(),
65 'post_content' => wp_json_encode( $action->get_args() ),
66 'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ),
67 'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ),
68 'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ),
69 );
70 return $post;
71 }
72
73 /**
74 * Save post array.
75 *
76 * @param array $post_array Post array.
77 * @return int Returns the post ID.
78 * @throws RuntimeException Throws an exception if the action could not be saved.
79 */
80 protected function save_post_array( $post_array ) {
81 add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
82 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
83
84 $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
85
86 if ( $has_kses ) {
87 // Prevent KSES from corrupting JSON in post_content.
88 kses_remove_filters();
89 }
90
91 $post_id = wp_insert_post( $post_array );
92
93 if ( $has_kses ) {
94 kses_init_filters();
95 }
96
97 remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
98 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
99
100 if ( is_wp_error( $post_id ) || empty( $post_id ) ) {
101 throw new RuntimeException( __( 'Unable to save action.', 'action-scheduler' ) );
102 }
103 return $post_id;
104 }
105
106 /**
107 * Filter insert post data.
108 *
109 * @param array $postdata Post data to filter.
110 *
111 * @return array
112 */
113 public function filter_insert_post_data( $postdata ) {
114 if ( self::POST_TYPE === $postdata['post_type'] ) {
115 $postdata['post_author'] = 0;
116 if ( 'future' === $postdata['post_status'] ) {
117 $postdata['post_status'] = 'publish';
118 }
119 }
120 return $postdata;
121 }
122
123 /**
124 * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug().
125 *
126 * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish'
127 * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug()
128 * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing
129 * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a
130 * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a
131 * database containing thousands of related post_name values.
132 *
133 * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue.
134 *
135 * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This
136 * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a
137 * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an
138 * action's slug, being probably unique is good enough.
139 *
140 * For more backstory on this issue, see:
141 * - https://github.com/woocommerce/action-scheduler/issues/44 and
142 * - https://core.trac.wordpress.org/ticket/21112
143 *
144 * @param string $override_slug Short-circuit return value.
145 * @param string $slug The desired slug (post_name).
146 * @param int $post_ID Post ID.
147 * @param string $post_status The post status.
148 * @param string $post_type Post type.
149 * @return string
150 */
151 public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
152 if ( self::POST_TYPE === $post_type ) {
153 $override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false );
154 }
155 return $override_slug;
156 }
157
158 /**
159 * Save post schedule.
160 *
161 * @param int $post_id Post ID of the scheduled action.
162 * @param string $schedule Schedule to save.
163 *
164 * @return void
165 */
166 protected function save_post_schedule( $post_id, $schedule ) {
167 update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule );
168 }
169
170 /**
171 * Save action group.
172 *
173 * @param int $post_id Post ID.
174 * @param string $group Group to save.
175 * @return void
176 */
177 protected function save_action_group( $post_id, $group ) {
178 if ( empty( $group ) ) {
179 wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, false );
180 } else {
181 wp_set_object_terms( $post_id, array( $group ), self::GROUP_TAXONOMY, false );
182 }
183 }
184
185 /**
186 * Fetch actions.
187 *
188 * @param int $action_id Action ID.
189 * @return object
190 */
191 public function fetch_action( $action_id ) {
192 $post = $this->get_post( $action_id );
193 if ( empty( $post ) || self::POST_TYPE !== $post->post_type ) {
194 return $this->get_null_action();
195 }
196
197 try {
198 $action = $this->make_action_from_post( $post );
199 } catch ( ActionScheduler_InvalidActionException $exception ) {
200 do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception );
201 return $this->get_null_action();
202 }
203
204 return $action;
205 }
206
207 /**
208 * Get post.
209 *
210 * @param string $action_id - Action ID.
211 * @return WP_Post|null
212 */
213 protected function get_post( $action_id ) {
214 if ( empty( $action_id ) ) {
215 return null;
216 }
217 return get_post( $action_id );
218 }
219
220 /**
221 * Get NULL action.
222 *
223 * @return ActionScheduler_NullAction
224 */
225 protected function get_null_action() {
226 return new ActionScheduler_NullAction();
227 }
228
229 /**
230 * Make action from post.
231 *
232 * @param WP_Post $post Post object.
233 * @return WP_Post
234 */
235 protected function make_action_from_post( $post ) {
236 $hook = $post->post_title;
237
238 $args = json_decode( $post->post_content, true );
239 $this->validate_args( $args, $post->ID );
240
241 $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true );
242 $this->validate_schedule( $schedule, $post->ID );
243
244 $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array( 'fields' => 'names' ) );
245 $group = empty( $group ) ? '' : reset( $group );
246
247 return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group );
248 }
249
250 /**
251 * Get action status by post status.
252 *
253 * @param string $post_status Post status.
254 *
255 * @throws InvalidArgumentException Throw InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels().
256 * @return string
257 */
258 protected function get_action_status_by_post_status( $post_status ) {
259
260 switch ( $post_status ) {
261 case 'publish':
262 $action_status = self::STATUS_COMPLETE;
263 break;
264 case 'trash':
265 $action_status = self::STATUS_CANCELED;
266 break;
267 default:
268 if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) {
269 throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) );
270 }
271 $action_status = $post_status;
272 break;
273 }
274
275 return $action_status;
276 }
277
278 /**
279 * Get post status by action status.
280 *
281 * @param string $action_status Action status.
282 *
283 * @throws InvalidArgumentException Throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels().
284 * @return string
285 */
286 protected function get_post_status_by_action_status( $action_status ) {
287
288 switch ( $action_status ) {
289 case self::STATUS_COMPLETE:
290 $post_status = 'publish';
291 break;
292 case self::STATUS_CANCELED:
293 $post_status = 'trash';
294 break;
295 default:
296 if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) {
297 throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) );
298 }
299 $post_status = $action_status;
300 break;
301 }
302
303 return $post_status;
304 }
305
306 /**
307 * Returns the SQL statement to query (or count) actions.
308 *
309 * @param array $query - Filtering options.
310 * @param string $select_or_count - Whether the SQL should select and return the IDs or just the row count.
311 *
312 * @throws InvalidArgumentException - Throw InvalidArgumentException if $select_or_count not count or select.
313 * @return string SQL statement. The returned SQL is already properly escaped.
314 */
315 protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
316
317 if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
318 throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) );
319 }
320
321 $query = wp_parse_args(
322 $query,
323 array(
324 'hook' => '',
325 'args' => null,
326 'date' => null,
327 'date_compare' => '<=',
328 'modified' => null,
329 'modified_compare' => '<=',
330 'group' => '',
331 'status' => '',
332 'claimed' => null,
333 'per_page' => 5,
334 'offset' => 0,
335 'orderby' => 'date',
336 'order' => 'ASC',
337 'search' => '',
338 )
339 );
340
341 /**
342 * Global wpdb object.
343 *
344 * @var wpdb $wpdb
345 */
346 global $wpdb;
347 $sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID ';
348 $sql .= "FROM {$wpdb->posts} p";
349 $sql_params = array();
350 if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) {
351 $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
352 $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
353 $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
354 } elseif ( ! empty( $query['group'] ) ) {
355 $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
356 $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
357 $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
358 $sql .= ' AND t.slug=%s';
359 $sql_params[] = $query['group'];
360 }
361 $sql .= ' WHERE post_type=%s';
362 $sql_params[] = self::POST_TYPE;
363 if ( $query['hook'] ) {
364 $sql .= ' AND p.post_title=%s';
365 $sql_params[] = $query['hook'];
366 }
367 if ( ! is_null( $query['args'] ) ) {
368 $sql .= ' AND p.post_content=%s';
369 $sql_params[] = wp_json_encode( $query['args'] );
370 }
371
372 if ( $query['status'] ) {
373 $post_statuses = array_map( array( $this, 'get_post_status_by_action_status' ), (array) $query['status'] );
374 $placeholders = array_fill( 0, count( $post_statuses ), '%s' );
375 $sql .= ' AND p.post_status IN (' . join( ', ', $placeholders ) . ')';
376 $sql_params = array_merge( $sql_params, array_values( $post_statuses ) );
377 }
378
379 if ( $query['date'] instanceof DateTime ) {
380 $date = clone $query['date'];
381 $date->setTimezone( new DateTimeZone( 'UTC' ) );
382 $date_string = $date->format( 'Y-m-d H:i:s' );
383 $comparator = $this->validate_sql_comparator( $query['date_compare'] );
384 $sql .= " AND p.post_date_gmt $comparator %s";
385 $sql_params[] = $date_string;
386 }
387
388 if ( $query['modified'] instanceof DateTime ) {
389 $modified = clone $query['modified'];
390 $modified->setTimezone( new DateTimeZone( 'UTC' ) );
391 $date_string = $modified->format( 'Y-m-d H:i:s' );
392 $comparator = $this->validate_sql_comparator( $query['modified_compare'] );
393 $sql .= " AND p.post_modified_gmt $comparator %s";
394 $sql_params[] = $date_string;
395 }
396
397 if ( true === $query['claimed'] ) {
398 $sql .= " AND p.post_password != ''";
399 } elseif ( false === $query['claimed'] ) {
400 $sql .= " AND p.post_password = ''";
401 } elseif ( ! is_null( $query['claimed'] ) ) {
402 $sql .= ' AND p.post_password = %s';
403 $sql_params[] = $query['claimed'];
404 }
405
406 if ( ! empty( $query['search'] ) ) {
407 $sql .= ' AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)';
408 for ( $i = 0; $i < 3; $i++ ) {
409 $sql_params[] = sprintf( '%%%s%%', $query['search'] );
410 }
411 }
412
413 if ( 'select' === $select_or_count ) {
414 switch ( $query['orderby'] ) {
415 case 'hook':
416 $orderby = 'p.post_title';
417 break;
418 case 'group':
419 $orderby = 't.name';
420 break;
421 case 'status':
422 $orderby = 'p.post_status';
423 break;
424 case 'modified':
425 $orderby = 'p.post_modified';
426 break;
427 case 'claim_id':
428 $orderby = 'p.post_password';
429 break;
430 case 'schedule':
431 case 'date':
432 default:
433 $orderby = 'p.post_date_gmt';
434 break;
435 }
436 if ( 'ASC' === strtoupper( $query['order'] ) ) {
437 $order = 'ASC';
438 } else {
439 $order = 'DESC';
440 }
441 $sql .= " ORDER BY $orderby $order";
442 if ( $query['per_page'] > 0 ) {
443 $sql .= ' LIMIT %d, %d';
444 $sql_params[] = $query['offset'];
445 $sql_params[] = $query['per_page'];
446 }
447 }
448
449 return $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
450 }
451
452 /**
453 * Query for action count or list of action IDs.
454 *
455 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
456 *
457 * @see ActionScheduler_Store::query_actions for $query arg usage.
458 *
459 * @param array $query Query filtering options.
460 * @param string $query_type Whether to select or count the results. Defaults to select.
461 *
462 * @return string|array|null The IDs of actions matching the query. Null on failure.
463 */
464 public function query_actions( $query = array(), $query_type = 'select' ) {
465 /**
466 * Global $wpdb object.
467 *
468 * @var wpdb $wpdb
469 */
470 global $wpdb;
471
472 $sql = $this->get_query_actions_sql( $query, $query_type );
473
474 return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
475 }
476
477 /**
478 * Get a count of all actions in the store, grouped by status
479 *
480 * @return array
481 */
482 public function action_counts() {
483
484 $action_counts_by_status = array();
485 $action_stati_and_labels = $this->get_status_labels();
486 $posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' );
487
488 foreach ( $posts_count_by_status as $post_status_name => $count ) {
489
490 try {
491 $action_status_name = $this->get_action_status_by_post_status( $post_status_name );
492 } catch ( Exception $e ) {
493 // Ignore any post statuses that aren't for actions.
494 continue;
495 }
496 if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) {
497 $action_counts_by_status[ $action_status_name ] = $count;
498 }
499 }
500
501 return $action_counts_by_status;
502 }
503
504 /**
505 * Cancel action.
506 *
507 * @param int $action_id Action ID.
508 *
509 * @throws InvalidArgumentException If $action_id is not identified.
510 */
511 public function cancel_action( $action_id ) {
512 $post = get_post( $action_id );
513 if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
514 /* translators: %s is the action ID */
515 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
516 }
517 do_action( 'action_scheduler_canceled_action', $action_id );
518 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
519 wp_trash_post( $action_id );
520 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
521 }
522
523 /**
524 * Delete action.
525 *
526 * @param int $action_id Action ID.
527 * @return void
528 * @throws InvalidArgumentException If action is not identified.
529 */
530 public function delete_action( $action_id ) {
531 $post = get_post( $action_id );
532 if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
533 /* translators: %s is the action ID */
534 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
535 }
536 do_action( 'action_scheduler_deleted_action', $action_id );
537
538 wp_delete_post( $action_id, true );
539 }
540
541 /**
542 * Get date for claim id.
543 *
544 * @param int $action_id Action ID.
545 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
546 */
547 public function get_date( $action_id ) {
548 $next = $this->get_date_gmt( $action_id );
549 return ActionScheduler_TimezoneHelper::set_local_timezone( $next );
550 }
551
552 /**
553 * Get Date GMT.
554 *
555 * @param int $action_id Action ID.
556 *
557 * @throws InvalidArgumentException If $action_id is not identified.
558 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
559 */
560 public function get_date_gmt( $action_id ) {
561 $post = get_post( $action_id );
562 if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
563 /* translators: %s is the action ID */
564 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
565 }
566 if ( 'publish' === $post->post_status ) {
567 return as_get_datetime_object( $post->post_modified_gmt );
568 } else {
569 return as_get_datetime_object( $post->post_date_gmt );
570 }
571 }
572
573 /**
574 * Stake claim.
575 *
576 * @param int $max_actions Maximum number of actions.
577 * @param DateTime $before_date Jobs must be schedule before this date. Defaults to now.
578 * @param array $hooks Claim only actions with a hook or hooks.
579 * @param string $group Claim only actions in the given group.
580 *
581 * @return ActionScheduler_ActionClaim
582 * @throws RuntimeException When there is an error staking a claim.
583 * @throws InvalidArgumentException When the given group is not valid.
584 */
585 public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
586 $this->claim_before_date = $before_date;
587 $claim_id = $this->generate_claim_id();
588 $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
589 $action_ids = $this->find_actions_by_claim_id( $claim_id );
590 $this->claim_before_date = null;
591
592 return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
593 }
594
595 /**
596 * Get claim count.
597 *
598 * @return int
599 */
600 public function get_claim_count() {
601 global $wpdb;
602
603 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
604 return $wpdb->get_var(
605 $wpdb->prepare(
606 "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')",
607 array( self::POST_TYPE )
608 )
609 );
610 }
611
612 /**
613 * Generate claim id.
614 *
615 * @return string
616 */
617 protected function generate_claim_id() {
618 $claim_id = md5( microtime( true ) . wp_rand( 0, 1000 ) );
619 return substr( $claim_id, 0, 20 ); // to fit in db field with 20 char limit.
620 }
621
622 /**
623 * Claim actions.
624 *
625 * @param string $claim_id Claim ID.
626 * @param int $limit Limit.
627 * @param DateTime $before_date Should use UTC timezone.
628 * @param array $hooks Claim only actions with a hook or hooks.
629 * @param string $group Claim only actions in the given group.
630 *
631 * @return int The number of actions that were claimed.
632 * @throws RuntimeException When there is a database error.
633 */
634 protected function claim_actions( $claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '' ) {
635 // Set up initial variables.
636 $date = null === $before_date ? as_get_datetime_object() : clone $before_date;
637 $limit_ids = ! empty( $group );
638 $ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array();
639
640 // If limiting by IDs and no posts found, then return early since we have nothing to update.
641 if ( $limit_ids && 0 === count( $ids ) ) {
642 return 0;
643 }
644
645 /**
646 * Global wpdb object.
647 *
648 * @var wpdb $wpdb
649 */
650 global $wpdb;
651
652 /*
653 * Build up custom query to update the affected posts. Parameters are built as a separate array
654 * to make it easier to identify where they are in the query.
655 *
656 * We can't use $wpdb->update() here because of the "ID IN ..." clause.
657 */
658 $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s";
659 $params = array(
660 $claim_id,
661 current_time( 'mysql', true ),
662 current_time( 'mysql' ),
663 );
664
665 // Build initial WHERE clause.
666 $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''";
667 $params[] = self::POST_TYPE;
668 $params[] = ActionScheduler_Store::STATUS_PENDING;
669
670 if ( ! empty( $hooks ) ) {
671 $placeholders = array_fill( 0, count( $hooks ), '%s' );
672 $where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')';
673 $params = array_merge( $params, array_values( $hooks ) );
674 }
675
676 /*
677 * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query.
678 *
679 * If we're not limiting by IDs, then include the post_date_gmt clause.
680 */
681 if ( $limit_ids ) {
682 $where .= ' AND ID IN (' . join( ',', $ids ) . ')';
683 } else {
684 $where .= ' AND post_date_gmt <= %s';
685 $params[] = $date->format( 'Y-m-d H:i:s' );
686 }
687
688 // Add the ORDER BY clause and,ms limit.
689 $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d';
690 $params[] = $limit;
691
692 // Run the query and gather results.
693 $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
694
695 if ( false === $rows_affected ) {
696 throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
697 }
698
699 return (int) $rows_affected;
700 }
701
702 /**
703 * Get IDs of actions within a certain group and up to a certain date/time.
704 *
705 * @param string $group The group to use in finding actions.
706 * @param int $limit The number of actions to retrieve.
707 * @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be
708 * up to and including this DateTime.
709 *
710 * @return array IDs of actions in the appropriate group and before the appropriate time.
711 * @throws InvalidArgumentException When the group does not exist.
712 */
713 protected function get_actions_by_group( $group, $limit, DateTime $date ) {
714 // Ensure the group exists before continuing.
715 if ( ! term_exists( $group, self::GROUP_TAXONOMY ) ) {
716 /* translators: %s is the group name */
717 throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
718 }
719
720 // Set up a query for post IDs to use later.
721 $query = new WP_Query();
722 $query_args = array(
723 'fields' => 'ids',
724 'post_type' => self::POST_TYPE,
725 'post_status' => ActionScheduler_Store::STATUS_PENDING,
726 'has_password' => false,
727 'posts_per_page' => $limit * 3,
728 'suppress_filters' => true,
729 'no_found_rows' => true,
730 'orderby' => array(
731 'menu_order' => 'ASC',
732 'date' => 'ASC',
733 'ID' => 'ASC',
734 ),
735 'date_query' => array(
736 'column' => 'post_date_gmt',
737 'before' => $date->format( 'Y-m-d H:i' ),
738 'inclusive' => true,
739 ),
740 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery
741 array(
742 'taxonomy' => self::GROUP_TAXONOMY,
743 'field' => 'slug',
744 'terms' => $group,
745 'include_children' => false,
746 ),
747 ),
748 );
749
750 return $query->query( $query_args );
751 }
752
753 /**
754 * Find actions by claim ID.
755 *
756 * @param string $claim_id Claim ID.
757 * @return array
758 */
759 public function find_actions_by_claim_id( $claim_id ) {
760 /**
761 * Global wpdb object.
762 *
763 * @var wpdb $wpdb
764 */
765 global $wpdb;
766
767 $action_ids = array();
768 $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
769 $cut_off = $before_date->format( 'Y-m-d H:i:s' );
770
771 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
772 $results = $wpdb->get_results(
773 $wpdb->prepare(
774 "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s",
775 array(
776 self::POST_TYPE,
777 $claim_id,
778 )
779 )
780 );
781
782 // Verify that the scheduled date for each action is within the expected bounds (in some unusual
783 // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
784 foreach ( $results as $claimed_action ) {
785 if ( $claimed_action->post_date_gmt <= $cut_off ) {
786 $action_ids[] = absint( $claimed_action->ID );
787 }
788 }
789
790 return $action_ids;
791 }
792
793 /**
794 * Release claim.
795 *
796 * @param ActionScheduler_ActionClaim $claim Claim object to release.
797 * @return void
798 * @throws RuntimeException When the claim is not unlocked.
799 */
800 public function release_claim( ActionScheduler_ActionClaim $claim ) {
801 $action_ids = $this->find_actions_by_claim_id( $claim->get_id() );
802 if ( empty( $action_ids ) ) {
803 return; // nothing to do.
804 }
805 $action_id_string = implode( ',', array_map( 'intval', $action_ids ) );
806 /**
807 * Global wpdb object.
808 *
809 * @var wpdb $wpdb
810 */
811 global $wpdb;
812
813 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
814 $result = $wpdb->query(
815 $wpdb->prepare(
816 "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s", //phpcs:ignore
817 array(
818 $claim->get_id(),
819 )
820 )
821 );
822 if ( false === $result ) {
823 /* translators: %s: claim ID */
824 throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'action-scheduler' ), $claim->get_id() ) );
825 }
826 }
827
828 /**
829 * Unclaim action.
830 *
831 * @param string $action_id Action ID.
832 * @throws RuntimeException When unable to unlock claim on action ID.
833 */
834 public function unclaim_action( $action_id ) {
835 /**
836 * Global wpdb object.
837 *
838 * @var wpdb $wpdb
839 */
840 global $wpdb;
841
842 //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
843 $result = $wpdb->query(
844 $wpdb->prepare(
845 "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s",
846 $action_id,
847 self::POST_TYPE
848 )
849 );
850 if ( false === $result ) {
851 /* translators: %s: action ID */
852 throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'action-scheduler' ), $action_id ) );
853 }
854 }
855
856 /**
857 * Mark failure on action.
858 *
859 * @param int $action_id Action ID.
860 *
861 * @return void
862 * @throws RuntimeException When unable to mark failure on action ID.
863 */
864 public function mark_failure( $action_id ) {
865 /**
866 * Global wpdb object.
867 *
868 * @var wpdb $wpdb
869 */
870 global $wpdb;
871
872 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
873 $result = $wpdb->query(
874 $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s", self::STATUS_FAILED, $action_id, self::POST_TYPE )
875 );
876 if ( false === $result ) {
877 /* translators: %s: action ID */
878 throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'action-scheduler' ), $action_id ) );
879 }
880 }
881
882 /**
883 * Return an action's claim ID, as stored in the post password column
884 *
885 * @param int $action_id Action ID.
886 * @return mixed
887 */
888 public function get_claim_id( $action_id ) {
889 return $this->get_post_column( $action_id, 'post_password' );
890 }
891
892 /**
893 * Return an action's status, as stored in the post status column
894 *
895 * @param int $action_id Action ID.
896 *
897 * @return mixed
898 * @throws InvalidArgumentException When the action ID is invalid.
899 */
900 public function get_status( $action_id ) {
901 $status = $this->get_post_column( $action_id, 'post_status' );
902
903 if ( null === $status ) {
904 throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
905 }
906
907 return $this->get_action_status_by_post_status( $status );
908 }
909
910 /**
911 * Get post column
912 *
913 * @param string $action_id Action ID.
914 * @param string $column_name Column Name.
915 *
916 * @return string|null
917 */
918 private function get_post_column( $action_id, $column_name ) {
919 /**
920 * Global wpdb object.
921 *
922 * @var wpdb $wpdb
923 */
924 global $wpdb;
925
926 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
927 return $wpdb->get_var(
928 $wpdb->prepare(
929 "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", // phpcs:ignore
930 $action_id,
931 self::POST_TYPE
932 )
933 );
934 }
935
936 /**
937 * Log Execution.
938 *
939 * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
940 *
941 * @param string $action_id Action ID.
942 */
943 public function log_execution( $action_id ) {
944 /**
945 * Global wpdb object.
946 *
947 * @var wpdb $wpdb
948 */
949 global $wpdb;
950
951 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
952 $status_updated = $wpdb->query(
953 $wpdb->prepare(
954 "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s",
955 self::STATUS_RUNNING,
956 current_time( 'mysql', true ),
957 current_time( 'mysql' ),
958 $action_id,
959 self::POST_TYPE
960 )
961 );
962
963 if ( ! $status_updated ) {
964 throw new Exception(
965 sprintf(
966 /* translators: 1: action ID. 2: status slug. */
967 __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
968 $action_id,
969 self::STATUS_RUNNING
970 )
971 );
972 }
973 }
974
975 /**
976 * Record that an action was completed.
977 *
978 * @param string $action_id ID of the completed action.
979 *
980 * @throws InvalidArgumentException When the action ID is invalid.
981 * @throws RuntimeException When there was an error executing the action.
982 */
983 public function mark_complete( $action_id ) {
984 $post = get_post( $action_id );
985 if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
986 /* translators: %s is the action ID */
987 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
988 }
989 add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
990 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
991 $result = wp_update_post(
992 array(
993 'ID' => $action_id,
994 'post_status' => 'publish',
995 ),
996 true
997 );
998 remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
999 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
1000 if ( is_wp_error( $result ) ) {
1001 throw new RuntimeException( $result->get_error_message() );
1002 }
1003
1004 /**
1005 * Fires after a scheduled action has been completed.
1006 *
1007 * @since 3.4.2
1008 *
1009 * @param int $action_id Action ID.
1010 */
1011 do_action( 'action_scheduler_completed_action', $action_id );
1012 }
1013
1014 /**
1015 * Mark action as migrated when there is an error deleting the action.
1016 *
1017 * @param int $action_id Action ID.
1018 */
1019 public function mark_migrated( $action_id ) {
1020 wp_update_post(
1021 array(
1022 'ID' => $action_id,
1023 'post_status' => 'migrated',
1024 )
1025 );
1026 }
1027
1028 /**
1029 * Determine whether the post store can be migrated.
1030 *
1031 * @param [type] $setting - Setting value.
1032 * @return bool
1033 */
1034 public function migration_dependencies_met( $setting ) {
1035 global $wpdb;
1036
1037 $dependencies_met = get_transient( self::DEPENDENCIES_MET );
1038 if ( empty( $dependencies_met ) ) {
1039 $maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 );
1040 $found_action = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1041 $wpdb->prepare(
1042 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1",
1043 $maximum_args_length,
1044 self::POST_TYPE
1045 )
1046 );
1047 $dependencies_met = $found_action ? 'no' : 'yes';
1048 set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS );
1049 }
1050
1051 return 'yes' === $dependencies_met ? $setting : false;
1052 }
1053
1054 /**
1055 * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4.
1056 *
1057 * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However,
1058 * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn
1059 * developers of this impending requirement.
1060 *
1061 * @param ActionScheduler_Action $action Action object.
1062 */
1063 protected function validate_action( ActionScheduler_Action $action ) {
1064 try {
1065 parent::validate_action( $action );
1066 } catch ( Exception $e ) {
1067 /* translators: %s is the error message */
1068 $message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'action-scheduler' ), $e->getMessage() );
1069 _doing_it_wrong( 'ActionScheduler_Action::$args', esc_html( $message ), '2.1.0' );
1070 }
1071 }
1072
1073 /**
1074 * (@codeCoverageIgnore)
1075 */
1076 public function init() {
1077 add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) );
1078
1079 $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar();
1080 $post_type_registrar->register();
1081
1082 $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar();
1083 $post_status_registrar->register();
1084
1085 $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar();
1086 $taxonomy_registrar->register();
1087 }
1088 }
1089