PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / vendor / woocommerce / action-scheduler / classes / data-stores / ActionScheduler_DBStore.php
mailpoet / vendor / woocommerce / action-scheduler / classes / data-stores Last commit date
ActionScheduler_DBLogger.php 1 year ago ActionScheduler_DBStore.php 1 year ago ActionScheduler_HybridStore.php 1 year ago ActionScheduler_wpCommentLogger.php 1 year ago ActionScheduler_wpPostStore.php 1 year ago ActionScheduler_wpPostStore_PostStatusRegistrar.php 4 years ago ActionScheduler_wpPostStore_PostTypeRegistrar.php 1 year ago ActionScheduler_wpPostStore_TaxonomyRegistrar.php 1 year ago index.php 3 years ago
ActionScheduler_DBStore.php
698 lines
1 <?php
2 if (!defined('ABSPATH')) exit;
3 class ActionScheduler_DBStore extends ActionScheduler_Store {
4 private $claim_before_date = null;
5 protected static $max_args_length = 8000;
6 protected static $max_index_length = 191;
7 protected $claim_filters = array(
8 'group' => '',
9 'hooks' => '',
10 'exclude-groups' => '',
11 );
12 public function init() {
13 $table_maker = new ActionScheduler_StoreSchema();
14 $table_maker->init();
15 $table_maker->register_tables();
16 }
17 public function save_unique_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
18 return $this->save_action_to_db( $action, $scheduled_date, true );
19 }
20 public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
21 return $this->save_action_to_db( $action, $scheduled_date, false );
22 }
23 private function save_action_to_db( ActionScheduler_Action $action, ?DateTime $date = null, $unique = false ) {
24 global $wpdb;
25 try {
26 $this->validate_action( $action );
27 $data = array(
28 'hook' => $action->get_hook(),
29 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ),
30 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ),
31 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
32 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
33 'group_id' => current( $this->get_group_ids( $action->get_group() ) ),
34 'priority' => $action->get_priority(),
35 );
36 $args = wp_json_encode( $action->get_args() );
37 if ( strlen( $args ) <= static::$max_index_length ) {
38 $data['args'] = $args;
39 } else {
40 $data['args'] = $this->hash_args( $args );
41 $data['extended_args'] = $args;
42 }
43 $insert_sql = $this->build_insert_sql( $data, $unique );
44 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_sql should be already prepared.
45 $wpdb->query( $insert_sql );
46 $action_id = $wpdb->insert_id;
47 if ( is_wp_error( $action_id ) ) {
48 throw new \RuntimeException( $action_id->get_error_message() );
49 } elseif ( empty( $action_id ) ) {
50 if ( $unique ) {
51 return 0;
52 }
53 throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) );
54 }
55 do_action( 'action_scheduler_stored_action', $action_id );
56 return $action_id;
57 } catch ( \Exception $e ) {
58 throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
59 }
60 }
61 private function build_insert_sql( array $data, $unique ) {
62 global $wpdb;
63 $columns = array_keys( $data );
64 $values = array_values( $data );
65 $placeholders = array_map( array( $this, 'get_placeholder_for_column' ), $columns );
66 $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions';
67 $column_sql = '`' . implode( '`, `', $columns ) . '`';
68 $placeholder_sql = implode( ', ', $placeholders );
69 $where_clause = $this->build_where_clause_for_insert( $data, $table_name, $unique );
70 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded.
71 $insert_query = $wpdb->prepare(
72 "
73 INSERT INTO $table_name ( $column_sql )
74 SELECT $placeholder_sql FROM DUAL
75 WHERE ( $where_clause ) IS NULL",
76 $values
77 );
78 // phpcs:enable
79 return $insert_query;
80 }
81 private function build_where_clause_for_insert( $data, $table_name, $unique ) {
82 global $wpdb;
83 if ( ! $unique ) {
84 return 'SELECT NULL FROM DUAL';
85 }
86 $pending_statuses = array(
87 ActionScheduler_Store::STATUS_PENDING,
88 ActionScheduler_Store::STATUS_RUNNING,
89 );
90 $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) );
91 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $pending_status_placeholders is hardcoded.
92 $where_clause = $wpdb->prepare(
93 "
94 SELECT action_id FROM $table_name
95 WHERE status IN ( $pending_status_placeholders )
96 AND hook = %s
97 AND `group_id` = %d
98 ",
99 array_merge(
100 $pending_statuses,
101 array(
102 $data['hook'],
103 $data['group_id'],
104 )
105 )
106 );
107 // phpcs:enable
108 return "$where_clause" . ' LIMIT 1';
109 }
110 private function get_placeholder_for_column( $column_name ) {
111 $string_columns = array(
112 'hook',
113 'status',
114 'scheduled_date_gmt',
115 'scheduled_date_local',
116 'args',
117 'schedule',
118 'last_attempt_gmt',
119 'last_attempt_local',
120 'extended_args',
121 );
122 return in_array( $column_name, $string_columns, true ) ? '%s' : '%d';
123 }
124 protected function hash_args( $args ) {
125 return md5( $args );
126 }
127 protected function get_args_for_query( $args ) {
128 $encoded = wp_json_encode( $args );
129 if ( strlen( $encoded ) <= static::$max_index_length ) {
130 return $encoded;
131 }
132 return $this->hash_args( $encoded );
133 }
134 protected function get_group_ids( $slugs, $create_if_not_exists = true ) {
135 $slugs = (array) $slugs;
136 $group_ids = array();
137 if ( empty( $slugs ) ) {
138 return array();
139 }
140 global $wpdb;
141 foreach ( $slugs as $slug ) {
142 $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
143 if ( empty( $group_id ) && $create_if_not_exists ) {
144 $group_id = $this->create_group( $slug );
145 }
146 if ( $group_id ) {
147 $group_ids[] = $group_id;
148 }
149 }
150 return $group_ids;
151 }
152 protected function create_group( $slug ) {
153 global $wpdb;
154 $wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) );
155 return (int) $wpdb->insert_id;
156 }
157 public function fetch_action( $action_id ) {
158 global $wpdb;
159 $data = $wpdb->get_row(
160 $wpdb->prepare(
161 "SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d",
162 $action_id
163 )
164 );
165 if ( empty( $data ) ) {
166 return $this->get_null_action();
167 }
168 if ( ! empty( $data->extended_args ) ) {
169 $data->args = $data->extended_args;
170 unset( $data->extended_args );
171 }
172 // Convert NULL dates to zero dates.
173 $date_fields = array(
174 'scheduled_date_gmt',
175 'scheduled_date_local',
176 'last_attempt_gmt',
177 'last_attempt_gmt',
178 );
179 foreach ( $date_fields as $date_field ) {
180 if ( is_null( $data->$date_field ) ) {
181 $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE;
182 }
183 }
184 try {
185 $action = $this->make_action_from_db_record( $data );
186 } catch ( ActionScheduler_InvalidActionException $exception ) {
187 do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception );
188 return $this->get_null_action();
189 }
190 return $action;
191 }
192 protected function get_null_action() {
193 return new ActionScheduler_NullAction();
194 }
195 protected function make_action_from_db_record( $data ) {
196 $hook = $data->hook;
197 $args = json_decode( $data->args, true );
198 $schedule = unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
199 $this->validate_args( $args, $data->action_id );
200 $this->validate_schedule( $schedule, $data->action_id );
201 if ( empty( $schedule ) ) {
202 $schedule = new ActionScheduler_NullSchedule();
203 }
204 $group = $data->group ? $data->group : '';
205 return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority );
206 }
207 protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
208 if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
209 throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) );
210 }
211 $query = wp_parse_args(
212 $query,
213 array(
214 'hook' => '',
215 'args' => null,
216 'partial_args_matching' => 'off', // can be 'like' or 'json'.
217 'date' => null,
218 'date_compare' => '<=',
219 'modified' => null,
220 'modified_compare' => '<=',
221 'group' => '',
222 'status' => '',
223 'claimed' => null,
224 'per_page' => 5,
225 'offset' => 0,
226 'orderby' => 'date',
227 'order' => 'ASC',
228 )
229 );
230 global $wpdb;
231 $db_server_info = is_callable( array( $wpdb, 'db_server_info' ) ) ? $wpdb->db_server_info() : $wpdb->db_version();
232 if ( false !== strpos( $db_server_info, 'MariaDB' ) ) {
233 $supports_json = version_compare(
234 PHP_VERSION_ID >= 80016 ? $wpdb->db_version() : preg_replace( '/[^0-9.].*/', '', str_replace( '5.5.5-', '', $db_server_info ) ),
235 '10.2',
236 '>='
237 );
238 } else {
239 $supports_json = version_compare( $wpdb->db_version(), '5.7', '>=' );
240 }
241 $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id';
242 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
243 $sql_params = array();
244 if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) {
245 $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id";
246 }
247 $sql .= ' WHERE 1=1';
248 if ( ! empty( $query['group'] ) ) {
249 $sql .= ' AND g.slug=%s';
250 $sql_params[] = $query['group'];
251 }
252 if ( ! empty( $query['hook'] ) ) {
253 $sql .= ' AND a.hook=%s';
254 $sql_params[] = $query['hook'];
255 }
256 if ( ! is_null( $query['args'] ) ) {
257 switch ( $query['partial_args_matching'] ) {
258 case 'json':
259 if ( ! $supports_json ) {
260 throw new \RuntimeException( __( 'JSON partial matching not supported in your environment. Please check your MySQL/MariaDB version.', 'action-scheduler' ) );
261 }
262 $supported_types = array(
263 'integer' => '%d',
264 'boolean' => '%s',
265 'double' => '%f',
266 'string' => '%s',
267 );
268 foreach ( $query['args'] as $key => $value ) {
269 $value_type = gettype( $value );
270 if ( 'boolean' === $value_type ) {
271 $value = $value ? 'true' : 'false';
272 }
273 $placeholder = isset( $supported_types[ $value_type ] ) ? $supported_types[ $value_type ] : false;
274 if ( ! $placeholder ) {
275 throw new \RuntimeException(
276 sprintf(
277 __( 'The value type for the JSON partial matching is not supported. Must be either integer, boolean, double or string. %s type provided.', 'action-scheduler' ),
278 $value_type
279 )
280 );
281 }
282 $sql .= ' AND JSON_EXTRACT(a.args, %s)=' . $placeholder;
283 $sql_params[] = '$.' . $key;
284 $sql_params[] = $value;
285 }
286 break;
287 case 'like':
288 foreach ( $query['args'] as $key => $value ) {
289 $sql .= ' AND a.args LIKE %s';
290 $json_partial = $wpdb->esc_like( trim( wp_json_encode( array( $key => $value ) ), '{}' ) );
291 $sql_params[] = "%{$json_partial}%";
292 }
293 break;
294 case 'off':
295 $sql .= ' AND a.args=%s';
296 $sql_params[] = $this->get_args_for_query( $query['args'] );
297 break;
298 default:
299 throw new \RuntimeException( __( 'Unknown partial args matching value.', 'action-scheduler' ) );
300 }
301 }
302 if ( $query['status'] ) {
303 $statuses = (array) $query['status'];
304 $placeholders = array_fill( 0, count( $statuses ), '%s' );
305 $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')';
306 $sql_params = array_merge( $sql_params, array_values( $statuses ) );
307 }
308 if ( $query['date'] instanceof \DateTime ) {
309 $date = clone $query['date'];
310 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
311 $date_string = $date->format( 'Y-m-d H:i:s' );
312 $comparator = $this->validate_sql_comparator( $query['date_compare'] );
313 $sql .= " AND a.scheduled_date_gmt $comparator %s";
314 $sql_params[] = $date_string;
315 }
316 if ( $query['modified'] instanceof \DateTime ) {
317 $modified = clone $query['modified'];
318 $modified->setTimezone( new \DateTimeZone( 'UTC' ) );
319 $date_string = $modified->format( 'Y-m-d H:i:s' );
320 $comparator = $this->validate_sql_comparator( $query['modified_compare'] );
321 $sql .= " AND a.last_attempt_gmt $comparator %s";
322 $sql_params[] = $date_string;
323 }
324 if ( true === $query['claimed'] ) {
325 $sql .= ' AND a.claim_id != 0';
326 } elseif ( false === $query['claimed'] ) {
327 $sql .= ' AND a.claim_id = 0';
328 } elseif ( ! is_null( $query['claimed'] ) ) {
329 $sql .= ' AND a.claim_id = %d';
330 $sql_params[] = $query['claimed'];
331 }
332 if ( ! empty( $query['search'] ) ) {
333 $sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s';
334 for ( $i = 0; $i < 3; $i++ ) {
335 $sql_params[] = sprintf( '%%%s%%', $query['search'] );
336 }
337 $search_claim_id = (int) $query['search'];
338 if ( $search_claim_id ) {
339 $sql .= ' OR a.claim_id = %d';
340 $sql_params[] = $search_claim_id;
341 }
342 $sql .= ')';
343 }
344 if ( 'select' === $select_or_count ) {
345 if ( 'ASC' === strtoupper( $query['order'] ) ) {
346 $order = 'ASC';
347 } else {
348 $order = 'DESC';
349 }
350 switch ( $query['orderby'] ) {
351 case 'hook':
352 $sql .= " ORDER BY a.hook $order";
353 break;
354 case 'group':
355 $sql .= " ORDER BY g.slug $order";
356 break;
357 case 'modified':
358 $sql .= " ORDER BY a.last_attempt_gmt $order";
359 break;
360 case 'none':
361 break;
362 case 'action_id':
363 $sql .= " ORDER BY a.action_id $order";
364 break;
365 case 'date':
366 default:
367 $sql .= " ORDER BY a.scheduled_date_gmt $order";
368 break;
369 }
370 if ( $query['per_page'] > 0 ) {
371 $sql .= ' LIMIT %d, %d';
372 $sql_params[] = $query['offset'];
373 $sql_params[] = $query['per_page'];
374 }
375 }
376 if ( ! empty( $sql_params ) ) {
377 $sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
378 }
379 return $sql;
380 }
381 public function query_actions( $query = array(), $query_type = 'select' ) {
382 global $wpdb;
383 $sql = $this->get_query_actions_sql( $query, $query_type );
384 return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoSql, WordPress.DB.DirectDatabaseQuery.NoCaching
385 }
386 public function action_counts() {
387 global $wpdb;
388 $sql = "SELECT a.status, count(a.status) as 'count'";
389 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
390 $sql .= ' GROUP BY a.status';
391 $actions_count_by_status = array();
392 $action_stati_and_labels = $this->get_status_labels();
393 foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
394 // Ignore any actions with invalid status.
395 if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) {
396 $actions_count_by_status[ $action_data->status ] = $action_data->count;
397 }
398 }
399 return $actions_count_by_status;
400 }
401 public function cancel_action( $action_id ) {
402 global $wpdb;
403 $updated = $wpdb->update(
404 $wpdb->actionscheduler_actions,
405 array( 'status' => self::STATUS_CANCELED ),
406 array( 'action_id' => $action_id ),
407 array( '%s' ),
408 array( '%d' )
409 );
410 if ( false === $updated ) {
411 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) );
412 }
413 do_action( 'action_scheduler_canceled_action', $action_id );
414 }
415 public function cancel_actions_by_hook( $hook ) {
416 $this->bulk_cancel_actions( array( 'hook' => $hook ) );
417 }
418 public function cancel_actions_by_group( $group ) {
419 $this->bulk_cancel_actions( array( 'group' => $group ) );
420 }
421 protected function bulk_cancel_actions( $query_args ) {
422 global $wpdb;
423 if ( ! is_array( $query_args ) ) {
424 return;
425 }
426 // Don't cancel actions that are already canceled.
427 if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) {
428 return;
429 }
430 $action_ids = true;
431 $query_args = wp_parse_args(
432 $query_args,
433 array(
434 'per_page' => 1000,
435 'status' => self::STATUS_PENDING,
436 'orderby' => 'none',
437 )
438 );
439 while ( $action_ids ) {
440 $action_ids = $this->query_actions( $query_args );
441 if ( empty( $action_ids ) ) {
442 break;
443 }
444 $format = array_fill( 0, count( $action_ids ), '%d' );
445 $query_in = '(' . implode( ',', $format ) . ')';
446 $parameters = $action_ids;
447 array_unshift( $parameters, self::STATUS_CANCELED );
448 $wpdb->query(
449 $wpdb->prepare(
450 "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
451 $parameters
452 )
453 );
454 do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
455 }
456 }
457 public function delete_action( $action_id ) {
458 global $wpdb;
459 $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) );
460 if ( empty( $deleted ) ) {
461 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) );
462 }
463 do_action( 'action_scheduler_deleted_action', $action_id );
464 }
465 public function get_date( $action_id ) {
466 $date = $this->get_date_gmt( $action_id );
467 ActionScheduler_TimezoneHelper::set_local_timezone( $date );
468 return $date;
469 }
470 protected function get_date_gmt( $action_id ) {
471 global $wpdb;
472 $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) );
473 if ( empty( $record ) ) {
474 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) );
475 }
476 if ( self::STATUS_PENDING === $record->status ) {
477 return as_get_datetime_object( $record->scheduled_date_gmt );
478 } else {
479 return as_get_datetime_object( $record->last_attempt_gmt );
480 }
481 }
482 public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
483 $claim_id = $this->generate_claim_id();
484 $this->claim_before_date = $before_date;
485 $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
486 $action_ids = $this->find_actions_by_claim_id( $claim_id );
487 $this->claim_before_date = null;
488 return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
489 }
490 protected function generate_claim_id() {
491 global $wpdb;
492 $now = as_get_datetime_object();
493 $wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) );
494 return $wpdb->insert_id;
495 }
496 public function set_claim_filter( $filter_name, $filter_values ) {
497 if ( isset( $this->claim_filters[ $filter_name ] ) ) {
498 $this->claim_filters[ $filter_name ] = $filter_values;
499 }
500 }
501 public function get_claim_filter( $filter_name ) {
502 if ( isset( $this->claim_filters[ $filter_name ] ) ) {
503 return $this->claim_filters[ $filter_name ];
504 }
505 return '';
506 }
507 protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
508 global $wpdb;
509 $now = as_get_datetime_object();
510 $date = is_null( $before_date ) ? $now : clone $before_date;
511 // can't use $wpdb->update() because of the <= condition.
512 $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
513 $params = array(
514 $claim_id,
515 $now->format( 'Y-m-d H:i:s' ),
516 current_time( 'mysql' ),
517 );
518 // Set claim filters.
519 if ( ! empty( $hooks ) ) {
520 $this->set_claim_filter( 'hooks', $hooks );
521 } else {
522 $hooks = $this->get_claim_filter( 'hooks' );
523 }
524 if ( ! empty( $group ) ) {
525 $this->set_claim_filter( 'group', $group );
526 } else {
527 $group = $this->get_claim_filter( 'group' );
528 }
529 $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
530 $params[] = $date->format( 'Y-m-d H:i:s' );
531 $params[] = self::STATUS_PENDING;
532 if ( ! empty( $hooks ) ) {
533 $placeholders = array_fill( 0, count( $hooks ), '%s' );
534 $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
535 $params = array_merge( $params, array_values( $hooks ) );
536 }
537 $group_operator = 'IN';
538 if ( empty( $group ) ) {
539 $group = $this->get_claim_filter( 'exclude-groups' );
540 $group_operator = 'NOT IN';
541 }
542 if ( ! empty( $group ) ) {
543 $group_ids = $this->get_group_ids( $group, false );
544 // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour.
545 if ( empty( $group_ids ) ) {
546 throw new InvalidArgumentException(
547 sprintf(
548 _n(
549 'The group "%s" does not exist.',
550 'The groups "%s" do not exist.',
551 is_array( $group ) ? count( $group ) : 1,
552 'action-scheduler'
553 ),
554 $group
555 )
556 );
557 }
558 $id_list = implode( ',', array_map( 'intval', $group_ids ) );
559 $where .= " AND group_id {$group_operator} ( $id_list )";
560 }
561 $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC', $claim_id, $hooks );
562 $params[] = $limit;
563 $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
564 $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
565 if ( false === $rows_affected ) {
566 $error = empty( $wpdb->last_error )
567 ? _x( 'unknown', 'database error', 'action-scheduler' )
568 : $wpdb->last_error;
569 throw new \RuntimeException(
570 sprintf(
571 __( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ),
572 $error
573 )
574 );
575 }
576 return (int) $rows_affected;
577 }
578 public function get_claim_count() {
579 global $wpdb;
580 $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)";
581 $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
582 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
583 }
584 public function get_claim_id( $action_id ) {
585 global $wpdb;
586 $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
587 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
588 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
589 }
590 public function find_actions_by_claim_id( $claim_id ) {
591 global $wpdb;
592 $action_ids = array();
593 $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
594 $cut_off = $before_date->format( 'Y-m-d H:i:s' );
595 $sql = $wpdb->prepare(
596 "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC",
597 $claim_id
598 );
599 // Verify that the scheduled date for each action is within the expected bounds (in some unusual
600 // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
601 foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
602 if ( $claimed_action->scheduled_date_gmt <= $cut_off ) {
603 $action_ids[] = absint( $claimed_action->action_id );
604 }
605 }
606 return $action_ids;
607 }
608 public function release_claim( ActionScheduler_ActionClaim $claim ) {
609 global $wpdb;
610 $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", $claim->get_id() ) );
611 $row_updates = 0;
612 if ( count( $action_ids ) > 0 ) {
613 $action_id_string = implode( ',', array_map( 'absint', $action_ids ) );
614 $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
615 }
616 $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) );
617 if ( $row_updates < count( $action_ids ) ) {
618 throw new RuntimeException(
619 sprintf(
620 // translators: %d is an id.
621 __( 'Unable to release actions from claim id %d.', 'action-scheduler' ),
622 $claim->get_id()
623 )
624 );
625 }
626 }
627 public function unclaim_action( $action_id ) {
628 global $wpdb;
629 $wpdb->update(
630 $wpdb->actionscheduler_actions,
631 array( 'claim_id' => 0 ),
632 array( 'action_id' => $action_id ),
633 array( '%s' ),
634 array( '%d' )
635 );
636 }
637 public function mark_failure( $action_id ) {
638 global $wpdb;
639 $updated = $wpdb->update(
640 $wpdb->actionscheduler_actions,
641 array( 'status' => self::STATUS_FAILED ),
642 array( 'action_id' => $action_id ),
643 array( '%s' ),
644 array( '%d' )
645 );
646 if ( empty( $updated ) ) {
647 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having failed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) );
648 }
649 }
650 public function log_execution( $action_id ) {
651 global $wpdb;
652 $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
653 $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
654 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
655 $status_updated = $wpdb->query( $sql );
656 if ( ! $status_updated ) {
657 throw new Exception(
658 sprintf(
659 __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
660 $action_id,
661 self::STATUS_RUNNING
662 )
663 );
664 }
665 }
666 public function mark_complete( $action_id ) {
667 global $wpdb;
668 $updated = $wpdb->update(
669 $wpdb->actionscheduler_actions,
670 array(
671 'status' => self::STATUS_COMPLETE,
672 'last_attempt_gmt' => current_time( 'mysql', true ),
673 'last_attempt_local' => current_time( 'mysql' ),
674 ),
675 array( 'action_id' => $action_id ),
676 array( '%s' ),
677 array( '%d' )
678 );
679 if ( empty( $updated ) ) {
680 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) );
681 }
682 do_action( 'action_scheduler_completed_action', $action_id );
683 }
684 public function get_status( $action_id ) {
685 global $wpdb;
686 $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
687 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
688 $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
689 if ( is_null( $status ) ) {
690 throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
691 } elseif ( empty( $status ) ) {
692 throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) );
693 } else {
694 return $status;
695 }
696 }
697 }
698