PluginProbe ʕ •ᴥ•ʔ
WP Mail SMTP by WPForms – The Most Popular SMTP and Email Log Plugin / 3.0.3
WP Mail SMTP by WPForms – The Most Popular SMTP and Email Log Plugin v3.0.3
4.9.0 0.9.6 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.2 1.7.0 1.7.1 1.8.0 1.8.1 1.9.0 2.0.0 2.0.1 2.1.1 2.2.1 2.3.1 2.4.0 2.5.0 2.5.1 2.6.0 2.7.0 2.8.0 2.9.0 3.0.1 3.0.2 3.0.3 3.1.0 3.10.0 3.11.0 3.11.1 3.2.0 3.2.1 3.3.0 3.4.0 3.5.0 3.5.1 3.5.2 3.6.1 3.7.0 3.8.0 3.8.2 3.9.0 4.0.1 4.1.0 4.1.1 4.2.0 4.3.0 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.8.0 trunk 0.10.0 0.10.1 0.11.1 0.11.2 0.3.1 0.3.2 0.4 0.4.1 0.4.2 0.5.0 0.5.1 0.5.2 0.6 0.7 0.8 0.8.2 0.8.3 0.8.4 0.8.5 0.8.6 0.8.7 0.9.0 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5
wp-mail-smtp / vendor / woocommerce / action-scheduler / classes / data-stores / ActionScheduler_wpPostStore.php
wp-mail-smtp / vendor / woocommerce / action-scheduler / classes / data-stores Last commit date
ActionScheduler_DBLogger.php 4 years ago ActionScheduler_DBStore.php 4 years ago ActionScheduler_HybridStore.php 4 years ago ActionScheduler_wpCommentLogger.php 4 years ago ActionScheduler_wpPostStore.php 4 years ago ActionScheduler_wpPostStore_PostStatusRegistrar.php 4 years ago ActionScheduler_wpPostStore_PostTypeRegistrar.php 4 years ago ActionScheduler_wpPostStore_TaxonomyRegistrar.php 4 years ago
ActionScheduler_wpPostStore.php
886 lines
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 /** @var DateTimeZone */
23 protected $local_timezone = NULL;
24
25 public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ){
26 try {
27 $this->validate_action( $action );
28 $post_array = $this->create_post_array( $action, $scheduled_date );
29 $post_id = $this->save_post_array( $post_array );
30 $this->save_post_schedule( $post_id, $action->get_schedule() );
31 $this->save_action_group( $post_id, $action->get_group() );
32 do_action( 'action_scheduler_stored_action', $post_id );
33 return $post_id;
34 } catch ( Exception $e ) {
35 throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
36 }
37 }
38
39 protected function create_post_array( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
40 $post = array(
41 'post_type' => self::POST_TYPE,
42 'post_title' => $action->get_hook(),
43 'post_content' => json_encode($action->get_args()),
44 'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ),
45 'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ),
46 'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ),
47 );
48 return $post;
49 }
50
51 protected function save_post_array( $post_array ) {
52 add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
53 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
54
55 $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
56
57 if ( $has_kses ) {
58 // Prevent KSES from corrupting JSON in post_content.
59 kses_remove_filters();
60 }
61
62 $post_id = wp_insert_post($post_array);
63
64 if ( $has_kses ) {
65 kses_init_filters();
66 }
67
68 remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
69 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
70
71 if ( is_wp_error($post_id) || empty($post_id) ) {
72 throw new RuntimeException( __( 'Unable to save action.', 'action-scheduler' ) );
73 }
74 return $post_id;
75 }
76
77 public function filter_insert_post_data( $postdata ) {
78 if ( $postdata['post_type'] == self::POST_TYPE ) {
79 $postdata['post_author'] = 0;
80 if ( $postdata['post_status'] == 'future' ) {
81 $postdata['post_status'] = 'publish';
82 }
83 }
84 return $postdata;
85 }
86
87 /**
88 * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug().
89 *
90 * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish'
91 * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug()
92 * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing
93 * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a
94 * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a
95 * database containing thousands of related post_name values.
96 *
97 * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue.
98 *
99 * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This
100 * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a
101 * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an
102 * action's slug, being probably unique is good enough.
103 *
104 * For more backstory on this issue, see:
105 * - https://github.com/woocommerce/action-scheduler/issues/44 and
106 * - https://core.trac.wordpress.org/ticket/21112
107 *
108 * @param string $override_slug Short-circuit return value.
109 * @param string $slug The desired slug (post_name).
110 * @param int $post_ID Post ID.
111 * @param string $post_status The post status.
112 * @param string $post_type Post type.
113 * @return string
114 */
115 public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
116 if ( self::POST_TYPE == $post_type ) {
117 $override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false );
118 }
119 return $override_slug;
120 }
121
122 protected function save_post_schedule( $post_id, $schedule ) {
123 update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule );
124 }
125
126 protected function save_action_group( $post_id, $group ) {
127 if ( empty($group) ) {
128 wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, FALSE );
129 } else {
130 wp_set_object_terms( $post_id, array($group), self::GROUP_TAXONOMY, FALSE );
131 }
132 }
133
134 public function fetch_action( $action_id ) {
135 $post = $this->get_post( $action_id );
136 if ( empty($post) || $post->post_type != self::POST_TYPE ) {
137 return $this->get_null_action();
138 }
139
140 try {
141 $action = $this->make_action_from_post( $post );
142 } catch ( ActionScheduler_InvalidActionException $exception ) {
143 do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception );
144 return $this->get_null_action();
145 }
146
147 return $action;
148 }
149
150 protected function get_post( $action_id ) {
151 if ( empty($action_id) ) {
152 return NULL;
153 }
154 return get_post($action_id);
155 }
156
157 protected function get_null_action() {
158 return new ActionScheduler_NullAction();
159 }
160
161 protected function make_action_from_post( $post ) {
162 $hook = $post->post_title;
163
164 $args = json_decode( $post->post_content, true );
165 $this->validate_args( $args, $post->ID );
166
167 $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true );
168 $this->validate_schedule( $schedule, $post->ID );
169
170 $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') );
171 $group = empty( $group ) ? '' : reset($group);
172
173 return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group );
174 }
175
176 /**
177 * @param string $post_status
178 *
179 * @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels()
180 * @return string
181 */
182 protected function get_action_status_by_post_status( $post_status ) {
183
184 switch ( $post_status ) {
185 case 'publish' :
186 $action_status = self::STATUS_COMPLETE;
187 break;
188 case 'trash' :
189 $action_status = self::STATUS_CANCELED;
190 break;
191 default :
192 if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) {
193 throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) );
194 }
195 $action_status = $post_status;
196 break;
197 }
198
199 return $action_status;
200 }
201
202 /**
203 * @param string $action_status
204 * @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels()
205 * @return string
206 */
207 protected function get_post_status_by_action_status( $action_status ) {
208
209 switch ( $action_status ) {
210 case self::STATUS_COMPLETE :
211 $post_status = 'publish';
212 break;
213 case self::STATUS_CANCELED :
214 $post_status = 'trash';
215 break;
216 default :
217 if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) {
218 throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) );
219 }
220 $post_status = $action_status;
221 break;
222 }
223
224 return $post_status;
225 }
226
227 /**
228 * @param string $hook
229 * @param array $params
230 *
231 * @return string ID of the next action matching the criteria or NULL if not found
232 */
233 public function find_action( $hook, $params = array() ) {
234 $params = wp_parse_args( $params, array(
235 'args' => NULL,
236 'status' => ActionScheduler_Store::STATUS_PENDING,
237 'group' => '',
238 ));
239 /** @var wpdb $wpdb */
240 global $wpdb;
241 $query = "SELECT p.ID FROM {$wpdb->posts} p";
242 $args = array();
243 if ( !empty($params['group']) ) {
244 $query .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
245 $query .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
246 $query .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id AND t.slug=%s";
247 $args[] = $params['group'];
248 }
249 $query .= " WHERE p.post_title=%s";
250 $args[] = $hook;
251 $query .= " AND p.post_type=%s";
252 $args[] = self::POST_TYPE;
253 if ( !is_null($params['args']) ) {
254 $query .= " AND p.post_content=%s";
255 $args[] = json_encode($params['args']);
256 }
257
258 if ( ! empty( $params['status'] ) ) {
259 $query .= " AND p.post_status=%s";
260 $args[] = $this->get_post_status_by_action_status( $params['status'] );
261 }
262
263 switch ( $params['status'] ) {
264 case self::STATUS_COMPLETE:
265 case self::STATUS_RUNNING:
266 case self::STATUS_FAILED:
267 $order = 'DESC'; // Find the most recent action that matches
268 break;
269 case self::STATUS_PENDING:
270 default:
271 $order = 'ASC'; // Find the next action that matches
272 break;
273 }
274 $query .= " ORDER BY post_date_gmt $order LIMIT 1";
275
276 $query = $wpdb->prepare( $query, $args );
277
278 $id = $wpdb->get_var($query);
279 return $id;
280 }
281
282 /**
283 * Returns the SQL statement to query (or count) actions.
284 *
285 * @param array $query Filtering options
286 * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count
287 * @throws InvalidArgumentException if $select_or_count not count or select
288 * @return string SQL statement. The returned SQL is already properly escaped.
289 */
290 protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
291
292 if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) {
293 throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) );
294 }
295
296 $query = wp_parse_args( $query, array(
297 'hook' => '',
298 'args' => NULL,
299 'date' => NULL,
300 'date_compare' => '<=',
301 'modified' => NULL,
302 'modified_compare' => '<=',
303 'group' => '',
304 'status' => '',
305 'claimed' => NULL,
306 'per_page' => 5,
307 'offset' => 0,
308 'orderby' => 'date',
309 'order' => 'ASC',
310 'search' => '',
311 ) );
312
313 /** @var wpdb $wpdb */
314 global $wpdb;
315 $sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID ';
316 $sql .= "FROM {$wpdb->posts} p";
317 $sql_params = array();
318 if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) {
319 $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
320 $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
321 $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
322 } elseif ( ! empty( $query['group'] ) ) {
323 $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
324 $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
325 $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
326 $sql .= " AND t.slug=%s";
327 $sql_params[] = $query['group'];
328 }
329 $sql .= " WHERE post_type=%s";
330 $sql_params[] = self::POST_TYPE;
331 if ( $query['hook'] ) {
332 $sql .= " AND p.post_title=%s";
333 $sql_params[] = $query['hook'];
334 }
335 if ( !is_null($query['args']) ) {
336 $sql .= " AND p.post_content=%s";
337 $sql_params[] = json_encode($query['args']);
338 }
339
340 if ( ! empty( $query['status'] ) ) {
341 $sql .= " AND p.post_status=%s";
342 $sql_params[] = $this->get_post_status_by_action_status( $query['status'] );
343 }
344
345 if ( $query['date'] instanceof DateTime ) {
346 $date = clone $query['date'];
347 $date->setTimezone( new DateTimeZone('UTC') );
348 $date_string = $date->format('Y-m-d H:i:s');
349 $comparator = $this->validate_sql_comparator($query['date_compare']);
350 $sql .= " AND p.post_date_gmt $comparator %s";
351 $sql_params[] = $date_string;
352 }
353
354 if ( $query['modified'] instanceof DateTime ) {
355 $modified = clone $query['modified'];
356 $modified->setTimezone( new DateTimeZone('UTC') );
357 $date_string = $modified->format('Y-m-d H:i:s');
358 $comparator = $this->validate_sql_comparator($query['modified_compare']);
359 $sql .= " AND p.post_modified_gmt $comparator %s";
360 $sql_params[] = $date_string;
361 }
362
363 if ( $query['claimed'] === TRUE ) {
364 $sql .= " AND p.post_password != ''";
365 } elseif ( $query['claimed'] === FALSE ) {
366 $sql .= " AND p.post_password = ''";
367 } elseif ( !is_null($query['claimed']) ) {
368 $sql .= " AND p.post_password = %s";
369 $sql_params[] = $query['claimed'];
370 }
371
372 if ( ! empty( $query['search'] ) ) {
373 $sql .= " AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)";
374 for( $i = 0; $i < 3; $i++ ) {
375 $sql_params[] = sprintf( '%%%s%%', $query['search'] );
376 }
377 }
378
379 if ( 'select' === $select_or_count ) {
380 switch ( $query['orderby'] ) {
381 case 'hook':
382 $orderby = 'p.post_title';
383 break;
384 case 'group':
385 $orderby = 't.name';
386 break;
387 case 'status':
388 $orderby = 'p.post_status';
389 break;
390 case 'modified':
391 $orderby = 'p.post_modified';
392 break;
393 case 'claim_id':
394 $orderby = 'p.post_password';
395 break;
396 case 'schedule':
397 case 'date':
398 default:
399 $orderby = 'p.post_date_gmt';
400 break;
401 }
402 if ( 'ASC' === strtoupper( $query['order'] ) ) {
403 $order = 'ASC';
404 } else {
405 $order = 'DESC';
406 }
407 $sql .= " ORDER BY $orderby $order";
408 if ( $query['per_page'] > 0 ) {
409 $sql .= " LIMIT %d, %d";
410 $sql_params[] = $query['offset'];
411 $sql_params[] = $query['per_page'];
412 }
413 }
414
415 return $wpdb->prepare( $sql, $sql_params );
416 }
417
418 /**
419 * @param array $query
420 * @param string $query_type Whether to select or count the results. Default, select.
421 * @return string|array The IDs of actions matching the query
422 */
423 public function query_actions( $query = array(), $query_type = 'select' ) {
424 /** @var wpdb $wpdb */
425 global $wpdb;
426
427 $sql = $this->get_query_actions_sql( $query, $query_type );
428
429 return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql );
430 }
431
432 /**
433 * Get a count of all actions in the store, grouped by status
434 *
435 * @return array
436 */
437 public function action_counts() {
438
439 $action_counts_by_status = array();
440 $action_stati_and_labels = $this->get_status_labels();
441 $posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' );
442
443 foreach ( $posts_count_by_status as $post_status_name => $count ) {
444
445 try {
446 $action_status_name = $this->get_action_status_by_post_status( $post_status_name );
447 } catch ( Exception $e ) {
448 // Ignore any post statuses that aren't for actions
449 continue;
450 }
451 if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) {
452 $action_counts_by_status[ $action_status_name ] = $count;
453 }
454 }
455
456 return $action_counts_by_status;
457 }
458
459 /**
460 * @param string $action_id
461 *
462 * @throws InvalidArgumentException
463 */
464 public function cancel_action( $action_id ) {
465 $post = get_post( $action_id );
466 if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) {
467 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
468 }
469 do_action( 'action_scheduler_canceled_action', $action_id );
470 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
471 wp_trash_post( $action_id );
472 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
473 }
474
475 public function delete_action( $action_id ) {
476 $post = get_post( $action_id );
477 if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) {
478 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
479 }
480 do_action( 'action_scheduler_deleted_action', $action_id );
481
482 wp_delete_post( $action_id, TRUE );
483 }
484
485 /**
486 * @param string $action_id
487 *
488 * @throws InvalidArgumentException
489 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
490 */
491 public function get_date( $action_id ) {
492 $next = $this->get_date_gmt( $action_id );
493 return ActionScheduler_TimezoneHelper::set_local_timezone( $next );
494 }
495
496 /**
497 * @param string $action_id
498 *
499 * @throws InvalidArgumentException
500 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
501 */
502 public function get_date_gmt( $action_id ) {
503 $post = get_post( $action_id );
504 if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) {
505 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
506 }
507 if ( $post->post_status == 'publish' ) {
508 return as_get_datetime_object( $post->post_modified_gmt );
509 } else {
510 return as_get_datetime_object( $post->post_date_gmt );
511 }
512 }
513
514 /**
515 * @param int $max_actions
516 * @param DateTime $before_date Jobs must be schedule before this date. Defaults to now.
517 * @param array $hooks Claim only actions with a hook or hooks.
518 * @param string $group Claim only actions in the given group.
519 *
520 * @return ActionScheduler_ActionClaim
521 * @throws RuntimeException When there is an error staking a claim.
522 * @throws InvalidArgumentException When the given group is not valid.
523 */
524 public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
525 $this->claim_before_date = $before_date;
526 $claim_id = $this->generate_claim_id();
527 $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
528 $action_ids = $this->find_actions_by_claim_id( $claim_id );
529 $this->claim_before_date = null;
530
531 return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
532 }
533
534 /**
535 * @return int
536 */
537 public function get_claim_count(){
538 global $wpdb;
539
540 $sql = "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')";
541 $sql = $wpdb->prepare( $sql, array( self::POST_TYPE ) );
542
543 return $wpdb->get_var( $sql );
544 }
545
546 protected function generate_claim_id() {
547 $claim_id = md5(microtime(true) . rand(0,1000));
548 return substr($claim_id, 0, 20); // to fit in db field with 20 char limit
549 }
550
551 /**
552 * @param string $claim_id
553 * @param int $limit
554 * @param DateTime $before_date Should use UTC timezone.
555 * @param array $hooks Claim only actions with a hook or hooks.
556 * @param string $group Claim only actions in the given group.
557 *
558 * @return int The number of actions that were claimed
559 * @throws RuntimeException When there is a database error.
560 * @throws InvalidArgumentException When the group is invalid.
561 */
562 protected function claim_actions( $claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '' ) {
563 // Set up initial variables.
564 $date = null === $before_date ? as_get_datetime_object() : clone $before_date;
565 $limit_ids = ! empty( $group );
566 $ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array();
567
568 // If limiting by IDs and no posts found, then return early since we have nothing to update.
569 if ( $limit_ids && 0 === count( $ids ) ) {
570 return 0;
571 }
572
573 /** @var wpdb $wpdb */
574 global $wpdb;
575
576 /*
577 * Build up custom query to update the affected posts. Parameters are built as a separate array
578 * to make it easier to identify where they are in the query.
579 *
580 * We can't use $wpdb->update() here because of the "ID IN ..." clause.
581 */
582 $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s";
583 $params = array(
584 $claim_id,
585 current_time( 'mysql', true ),
586 current_time( 'mysql' ),
587 );
588
589 // Build initial WHERE clause.
590 $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''";
591 $params[] = self::POST_TYPE;
592 $params[] = ActionScheduler_Store::STATUS_PENDING;
593
594 if ( ! empty( $hooks ) ) {
595 $placeholders = array_fill( 0, count( $hooks ), '%s' );
596 $where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')';
597 $params = array_merge( $params, array_values( $hooks ) );
598 }
599
600 /*
601 * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query.
602 *
603 * If we're not limiting by IDs, then include the post_date_gmt clause.
604 */
605 if ( $limit_ids ) {
606 $where .= ' AND ID IN (' . join( ',', $ids ) . ')';
607 } else {
608 $where .= ' AND post_date_gmt <= %s';
609 $params[] = $date->format( 'Y-m-d H:i:s' );
610 }
611
612 // Add the ORDER BY clause and,ms limit.
613 $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d';
614 $params[] = $limit;
615
616 // Run the query and gather results.
617 $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) );
618 if ( $rows_affected === false ) {
619 throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
620 }
621
622 return (int) $rows_affected;
623 }
624
625 /**
626 * Get IDs of actions within a certain group and up to a certain date/time.
627 *
628 * @param string $group The group to use in finding actions.
629 * @param int $limit The number of actions to retrieve.
630 * @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be
631 * up to and including this DateTime.
632 *
633 * @return array IDs of actions in the appropriate group and before the appropriate time.
634 * @throws InvalidArgumentException When the group does not exist.
635 */
636 protected function get_actions_by_group( $group, $limit, DateTime $date ) {
637 // Ensure the group exists before continuing.
638 if ( ! term_exists( $group, self::GROUP_TAXONOMY )) {
639 throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
640 }
641
642 // Set up a query for post IDs to use later.
643 $query = new WP_Query();
644 $query_args = array(
645 'fields' => 'ids',
646 'post_type' => self::POST_TYPE,
647 'post_status' => ActionScheduler_Store::STATUS_PENDING,
648 'has_password' => false,
649 'posts_per_page' => $limit * 3,
650 'suppress_filters' => true,
651 'no_found_rows' => true,
652 'orderby' => array(
653 'menu_order' => 'ASC',
654 'date' => 'ASC',
655 'ID' => 'ASC',
656 ),
657 'date_query' => array(
658 'column' => 'post_date_gmt',
659 'before' => $date->format( 'Y-m-d H:i' ),
660 'inclusive' => true,
661 ),
662 'tax_query' => array(
663 array(
664 'taxonomy' => self::GROUP_TAXONOMY,
665 'field' => 'slug',
666 'terms' => $group,
667 'include_children' => false,
668 ),
669 ),
670 );
671
672 return $query->query( $query_args );
673 }
674
675 /**
676 * @param string $claim_id
677 * @return array
678 */
679 public function find_actions_by_claim_id( $claim_id ) {
680 /** @var wpdb $wpdb */
681 global $wpdb;
682
683 $sql = "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s";
684 $sql = $wpdb->prepare( $sql, array( self::POST_TYPE, $claim_id ) );
685
686 $action_ids = array();
687 $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
688 $cut_off = $before_date->format( 'Y-m-d H:i:s' );
689
690 // Verify that the scheduled date for each action is within the expected bounds (in some unusual
691 // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
692 foreach ( $wpdb->get_results( $sql ) as $claimed_action ) {
693 if ( $claimed_action->post_date_gmt <= $cut_off ) {
694 $action_ids[] = absint( $claimed_action->ID );
695 }
696 }
697
698 return $action_ids;
699 }
700
701 public function release_claim( ActionScheduler_ActionClaim $claim ) {
702 $action_ids = $this->find_actions_by_claim_id( $claim->get_id() );
703 if ( empty( $action_ids ) ) {
704 return; // nothing to do
705 }
706 $action_id_string = implode( ',', array_map( 'intval', $action_ids ) );
707 /** @var wpdb $wpdb */
708 global $wpdb;
709 $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s";
710 $sql = $wpdb->prepare( $sql, array( $claim->get_id() ) );
711 $result = $wpdb->query( $sql );
712 if ( $result === false ) {
713 /* translators: %s: claim ID */
714 throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'action-scheduler' ), $claim->get_id() ) );
715 }
716 }
717
718 /**
719 * @param string $action_id
720 */
721 public function unclaim_action( $action_id ) {
722 /** @var wpdb $wpdb */
723 global $wpdb;
724 $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s";
725 $sql = $wpdb->prepare( $sql, $action_id, self::POST_TYPE );
726 $result = $wpdb->query( $sql );
727 if ( $result === false ) {
728 /* translators: %s: action ID */
729 throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'action-scheduler' ), $action_id ) );
730 }
731 }
732
733 public function mark_failure( $action_id ) {
734 /** @var wpdb $wpdb */
735 global $wpdb;
736 $sql = "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s";
737 $sql = $wpdb->prepare( $sql, self::STATUS_FAILED, $action_id, self::POST_TYPE );
738 $result = $wpdb->query( $sql );
739 if ( $result === false ) {
740 /* translators: %s: action ID */
741 throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'action-scheduler' ), $action_id ) );
742 }
743 }
744
745 /**
746 * Return an action's claim ID, as stored in the post password column
747 *
748 * @param string $action_id
749 * @return mixed
750 */
751 public function get_claim_id( $action_id ) {
752 return $this->get_post_column( $action_id, 'post_password' );
753 }
754
755 /**
756 * Return an action's status, as stored in the post status column
757 *
758 * @param string $action_id
759 * @return mixed
760 */
761 public function get_status( $action_id ) {
762 $status = $this->get_post_column( $action_id, 'post_status' );
763
764 if ( $status === null ) {
765 throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
766 }
767
768 return $this->get_action_status_by_post_status( $status );
769 }
770
771 private function get_post_column( $action_id, $column_name ) {
772 /** @var \wpdb $wpdb */
773 global $wpdb;
774 return $wpdb->get_var( $wpdb->prepare( "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", $action_id, self::POST_TYPE ) );
775 }
776
777 /**
778 * @param string $action_id
779 */
780 public function log_execution( $action_id ) {
781 /** @var wpdb $wpdb */
782 global $wpdb;
783
784 $sql = "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";
785 $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time('mysql', true), current_time('mysql'), $action_id, self::POST_TYPE );
786 $wpdb->query($sql);
787 }
788
789 /**
790 * Record that an action was completed.
791 *
792 * @param int $action_id ID of the completed action.
793 * @throws InvalidArgumentException|RuntimeException
794 */
795 public function mark_complete( $action_id ) {
796 $post = get_post( $action_id );
797 if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) {
798 throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
799 }
800 add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
801 add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
802 $result = wp_update_post(array(
803 'ID' => $action_id,
804 'post_status' => 'publish',
805 ), TRUE);
806 remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
807 remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
808 if ( is_wp_error( $result ) ) {
809 throw new RuntimeException( $result->get_error_message() );
810 }
811 }
812
813 /**
814 * Mark action as migrated when there is an error deleting the action.
815 *
816 * @param int $action_id Action ID.
817 */
818 public function mark_migrated( $action_id ) {
819 wp_update_post(
820 array(
821 'ID' => $action_id,
822 'post_status' => 'migrated'
823 )
824 );
825 }
826
827 /**
828 * Determine whether the post store can be migrated.
829 *
830 * @return bool
831 */
832 public function migration_dependencies_met( $setting ) {
833 global $wpdb;
834
835 $dependencies_met = get_transient( self::DEPENDENCIES_MET );
836 if ( empty( $dependencies_met ) ) {
837 $maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 );
838 $found_action = $wpdb->get_var(
839 $wpdb->prepare(
840 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1",
841 $maximum_args_length,
842 self::POST_TYPE
843 )
844 );
845 $dependencies_met = $found_action ? 'no' : 'yes';
846 set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS );
847 }
848
849 return 'yes' == $dependencies_met ? $setting : false;
850 }
851
852 /**
853 * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4.
854 *
855 * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However,
856 * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn
857 * developers of this impending requirement.
858 *
859 * @param ActionScheduler_Action $action
860 */
861 protected function validate_action( ActionScheduler_Action $action ) {
862 try {
863 parent::validate_action( $action );
864 } catch ( Exception $e ) {
865 $message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'action-scheduler' ), $e->getMessage() );
866 _doing_it_wrong( 'ActionScheduler_Action::$args', $message, '2.1.0' );
867 }
868 }
869
870 /**
871 * @codeCoverageIgnore
872 */
873 public function init() {
874 add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) );
875
876 $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar();
877 $post_type_registrar->register();
878
879 $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar();
880 $post_status_registrar->register();
881
882 $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar();
883 $taxonomy_registrar->register();
884 }
885 }
886