PluginProbe
WooCommerce / 11.1.0-beta.2
WooCommerce v11.1.0-beta.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / includes / data-stores / class-wc-data-store-wp.php
class-wc-data-store-wp.php
665 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared logic for WP based data.
4 * Contains functions like meta handling for all default data stores.
5 * Your own data store doesn't need to use WC_Data_Store_WP -- you can write
6 * your own meta handling functions.
7 *
8 * @version 3.0.0
9 * @package WooCommerce\Classes
10 */
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * WC_Data_Store_WP class.
16 */
17 class WC_Data_Store_WP {
18
19 /**
20 * Meta type. This should match up with
21 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
22 * WP defines 'post', 'user', 'comment', and 'term'.
23 *
24 * @var string
25 */
26 protected $meta_type = 'post';
27
28 /**
29 * This only needs set if you are using a custom metadata type (for example payment tokens.
30 * This should be the name of the field your table uses for associating meta with objects.
31 * For example, in payment_tokenmeta, this would be payment_token_id.
32 *
33 * @var string
34 */
35 protected $object_id_field_for_meta = '';
36
37 /**
38 * Data stored in meta keys, but not considered "meta" for an object.
39 *
40 * @since 3.0.0
41 *
42 * @var array
43 */
44 protected $internal_meta_keys = array();
45
46 /**
47 * Meta data which should exist in the DB, even if empty.
48 *
49 * @since 3.6.0
50 *
51 * @var array
52 */
53 protected $must_exist_meta_keys = array();
54
55 /**
56 * Get and store terms from a taxonomy.
57 *
58 * @since 3.0.0
59 * @param WC_Data|integer $object WC_Data object or object ID.
60 * @param string $taxonomy Taxonomy name e.g. product_cat.
61 * @return array of terms
62 */
63 protected function get_term_ids( $object, $taxonomy ) {
64 if ( is_numeric( $object ) ) {
65 $object_id = $object;
66 } else {
67 $object_id = $object->get_id();
68 }
69 $terms = get_the_terms( $object_id, $taxonomy );
70 if ( false === $terms || is_wp_error( $terms ) ) {
71 return array();
72 }
73 return wp_list_pluck( $terms, 'term_id' );
74 }
75
76 /**
77 * Returns an array of meta for an object.
78 *
79 * @since 3.0.0
80 * @param WC_Data $object WC_Data object.
81 * @return array
82 */
83 public function read_meta( &$object ) {
84 global $wpdb;
85 $db_info = $this->get_db_info();
86 $raw_meta_data = $wpdb->get_results(
87 $wpdb->prepare(
88 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
89 "SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value
90 FROM {$db_info['table']}
91 WHERE {$db_info['object_id_field']} = %d
92 ORDER BY {$db_info['meta_id_field']}",
93 // phpcs:enable
94 $object->get_id()
95 )
96 );
97 return $this->filter_raw_meta_data( $object, $raw_meta_data );
98 }
99
100 /**
101 * Helper method to filter internal meta keys from all meta data rows for the object.
102 *
103 * @since 4.7.0
104 *
105 * @param WC_Data $object WC_Data object.
106 * @param array $raw_meta_data Array of std object of meta data to be filtered.
107 *
108 * @return mixed|void
109 */
110 public function filter_raw_meta_data( &$object, $raw_meta_data ) {
111 $this->internal_meta_keys = array_unique(
112 array_merge(
113 array_map(
114 array( $this, 'prefix_key' ),
115 $object->get_data_keys()
116 ),
117 $this->internal_meta_keys
118 )
119 );
120 $meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
121 return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
122 }
123
124 /**
125 * Deletes meta based on meta ID.
126 *
127 * @since 3.0.0
128 * @param WC_Data $object WC_Data object.
129 * @param stdClass $meta (containing at least ->id).
130 */
131 public function delete_meta( &$object, $meta ) {
132 delete_metadata_by_mid( $this->meta_type, $meta->id );
133 }
134
135 /**
136 * Add new piece of meta.
137 *
138 * @since 3.0.0
139 * @param WC_Data $object WC_Data object.
140 * @param stdClass $meta (containing ->key and ->value).
141 * @return int meta ID
142 */
143 public function add_meta( &$object, $meta ) {
144 return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false );
145 }
146
147 /**
148 * Update meta.
149 *
150 * @since 3.0.0
151 * @param WC_Data $object WC_Data object.
152 * @param stdClass $meta (containing ->id, ->key and ->value).
153 */
154 public function update_meta( &$object, $meta ) {
155 update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key );
156 }
157
158 /**
159 * Table structure is slightly different between meta types, this function will return what we need to know.
160 *
161 * @since 3.0.0
162 * @return array Array elements: table, object_id_field, meta_id_field
163 */
164 protected function get_db_info() {
165 global $wpdb;
166
167 $meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
168 $table = $wpdb->prefix;
169
170 // If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
171 if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ), true ) ) {
172 $table .= 'woocommerce_';
173 }
174
175 $table .= $this->meta_type . 'meta';
176 $object_id_field = $this->meta_type . '_id';
177
178 // Figure out our field names.
179 if ( 'user' === $this->meta_type ) {
180 $meta_id_field = 'umeta_id';
181 $table = $wpdb->usermeta;
182 }
183
184 if ( ! empty( $this->object_id_field_for_meta ) ) {
185 $object_id_field = $this->object_id_field_for_meta;
186 }
187
188 return array(
189 'table' => $table,
190 'object_id_field' => $object_id_field,
191 'meta_id_field' => $meta_id_field,
192 );
193 }
194
195 /**
196 * Internal meta keys we don't want exposed as part of meta_data. This is in
197 * addition to all data props with _ prefix.
198 *
199 * @since 2.6.0
200 *
201 * @param string $key Prefix to be added to meta keys.
202 * @return string
203 */
204 protected function prefix_key( $key ) {
205 return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
206 }
207
208 /**
209 * Callback to remove unwanted meta data.
210 *
211 * @param object $meta Meta object to check if it should be excluded or not.
212 * @return bool
213 */
214 protected function exclude_internal_meta_keys( $meta ) {
215 return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' );
216 }
217
218 /**
219 * Gets a list of props and meta keys that need updated based on change state
220 * or if they are present in the database or not.
221 *
222 * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
223 * @param array $meta_key_to_props A mapping of meta keys => prop names.
224 * @param string $meta_type The internal WP meta type (post, user, etc).
225 * @return array A mapping of meta keys => prop names, filtered by ones that should be updated.
226 */
227 protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) {
228 $props_to_update = array();
229 $changed_props = $object->get_changes();
230
231 // Props should be updated if they are a part of the $changed array or don't exist yet.
232 foreach ( $meta_key_to_props as $meta_key => $prop ) {
233 if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) {
234 $props_to_update[ $meta_key ] = $prop;
235 }
236 }
237
238 return $props_to_update;
239 }
240
241 /**
242 * Update meta data in, or delete it from, the database.
243 *
244 * Avoids storing meta when it's either an empty string or empty array.
245 * Other empty values such as numeric 0 and null should still be stored.
246 * Data-stores can force meta to exist using `must_exist_meta_keys`.
247 *
248 * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
249 *
250 * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
251 * @param string $meta_key Meta key to update.
252 * @param mixed $meta_value Value to save.
253 *
254 * @since 3.6.0 Added to prevent empty meta being stored unless required.
255 *
256 * @return bool True if updated/deleted.
257 */
258 protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
259 if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
260 $updated = delete_post_meta( $object->get_id(), $meta_key );
261 } else {
262 $updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
263 }
264
265 return (bool) $updated;
266 }
267
268 /**
269 * Get valid WP_Query args from a WC_Object_Query's query variables.
270 *
271 * @since 3.1.0
272 * @param array $query_vars query vars from a WC_Object_Query.
273 * @return array
274 */
275 protected function get_wp_query_args( $query_vars ) {
276
277 $skipped_values = array( '', array(), null );
278 $wp_query_args = array(
279 'errors' => array(),
280 'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
281 );
282
283 foreach ( $query_vars as $key => $value ) {
284 if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) {
285 continue;
286 }
287
288 // Build meta queries out of vars that are stored in internal meta keys.
289 if ( in_array( '_' . $key, $this->internal_meta_keys, true ) ) {
290 // Check for existing values if wildcard is used.
291 if ( '*' === $value ) {
292 $wp_query_args['meta_query'][] = array(
293 array(
294 'key' => '_' . $key,
295 'compare' => 'EXISTS',
296 ),
297 array(
298 'key' => '_' . $key,
299 'value' => '',
300 'compare' => '!=',
301 ),
302 );
303 } else {
304 $wp_query_args['meta_query'][] = array(
305 'key' => '_' . $key,
306 'value' => $value,
307 'compare' => is_array( $value ) ? 'IN' : '=',
308 );
309 }
310 } else { // Other vars get mapped to wp_query args or just left alone.
311 $key_mapping = array(
312 'parent' => 'post_parent',
313 'parent_exclude' => 'post_parent__not_in',
314 'exclude' => 'post__not_in',
315 'limit' => 'posts_per_page',
316 'type' => 'post_type',
317 'return' => 'fields',
318 );
319
320 if ( isset( $key_mapping[ $key ] ) ) {
321 $wp_query_args[ $key_mapping[ $key ] ] = $value;
322 } else {
323 $wp_query_args[ $key ] = $value;
324 }
325 }
326 }
327
328 return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars );
329 }
330
331 /**
332 * Map a valid date query var to WP_Query arguments.
333 * Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators.
334 * Also accepts a WC_DateTime object.
335 *
336 * @since 3.2.0
337 * @param mixed $query_var A valid date format.
338 * @param string $key meta or db column key.
339 * @param array $wp_query_args WP_Query args.
340 * @return array Modified $wp_query_args
341 */
342 public function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) {
343 $query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';
344 $valid_operators = array( '>', '>=', '=', '<=', '<', '...' );
345
346 // YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision.
347 $precision = 'second';
348
349 $dates = array();
350 $operator = '=';
351
352 try {
353 // Specific time query with a WC_DateTime.
354 if ( is_a( $query_var, 'WC_DateTime' ) ) {
355 $dates[] = $query_var;
356 } elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp.
357 $dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) );
358 } elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates.
359 if ( ! empty( $sections[1] ) ) {
360 $dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] );
361 }
362
363 $operator = in_array( $sections[2], $valid_operators, true ) ? $sections[2] : '';
364 $dates[] = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] );
365
366 if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) {
367 $precision = 'day';
368 }
369 } else { // Specific time query with a string.
370 $dates[] = wc_string_to_datetime( $query_var );
371 $precision = 'day';
372 }
373 } catch ( Exception $e ) {
374 return $wp_query_args;
375 }
376
377 // Check for valid inputs.
378 if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) {
379 return $wp_query_args;
380 }
381
382 // Build date query for 'post_date' or 'post_modified' keys.
383 if ( 'post_date' === $key || 'post_modified' === $key ) {
384 if ( ! isset( $wp_query_args['date_query'] ) ) {
385 $wp_query_args['date_query'] = array();
386 }
387
388 $query_arg = array(
389 'column' => 'day' === $precision ? $key : $key . '_gmt',
390 'inclusive' => '>' !== $operator && '<' !== $operator,
391 );
392
393 // Add 'before'/'after' query args.
394 $comparisons = array();
395 if ( '>' === $operator || '>=' === $operator || '...' === $operator ) {
396 $comparisons[] = 'after';
397 }
398 if ( '<' === $operator || '<=' === $operator || '...' === $operator ) {
399 $comparisons[] = 'before';
400 }
401
402 foreach ( $comparisons as $index => $comparison ) {
403 if ( 'day' === $precision ) {
404 /**
405 * WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and
406 * 'after' string query, so we have to use the array format in 'day' precision.
407 *
408 * @see https://core.trac.wordpress.org/ticket/29908
409 */
410 $query_arg[ $comparison ]['year'] = $dates[ $index ]->date( 'Y' );
411 $query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' );
412 $query_arg[ $comparison ]['day'] = $dates[ $index ]->date( 'j' );
413 } else {
414 /**
415 * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries,
416 * so we have to use a string query.
417 */
418 $query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() );
419 }
420 }
421
422 if ( empty( $comparisons ) ) {
423 $query_arg['year'] = $dates[0]->date( 'Y' );
424 $query_arg['month'] = $dates[0]->date( 'n' );
425 $query_arg['day'] = $dates[0]->date( 'j' );
426 if ( 'second' === $precision ) {
427 $query_arg['hour'] = $dates[0]->date( 'H' );
428 $query_arg['minute'] = $dates[0]->date( 'i' );
429 $query_arg['second'] = $dates[0]->date( 's' );
430 }
431 }
432 $wp_query_args['date_query'][] = $query_arg;
433 return $wp_query_args;
434 }
435
436 // Build meta query for unrecognized keys.
437 if ( ! isset( $wp_query_args['meta_query'] ) ) {
438 $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
439 }
440
441 // Meta dates are stored as timestamps in the db.
442 // Check against beginning/end-of-day timestamps when using 'day' precision.
443 if ( 'day' === $precision ) {
444 $start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) );
445 $end_timestamp = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) );
446 switch ( $operator ) {
447 case '>':
448 case '<=':
449 $wp_query_args['meta_query'][] = array(
450 'key' => $key,
451 'value' => $end_timestamp,
452 'compare' => $operator,
453 );
454 break;
455 case '<':
456 case '>=':
457 $wp_query_args['meta_query'][] = array(
458 'key' => $key,
459 'value' => $start_timestamp,
460 'compare' => $operator,
461 );
462 break;
463 default:
464 $wp_query_args['meta_query'][] = array(
465 'key' => $key,
466 'value' => $start_timestamp,
467 'compare' => '>=',
468 );
469 $wp_query_args['meta_query'][] = array(
470 'key' => $key,
471 'value' => $end_timestamp,
472 'compare' => '<=',
473 );
474 }
475 } elseif ( '...' !== $operator ) {
476 $wp_query_args['meta_query'][] = array(
477 'key' => $key,
478 'value' => $dates[0]->getTimestamp(),
479 'compare' => $operator,
480 );
481 } else {
482 $wp_query_args['meta_query'][] = array(
483 'key' => $key,
484 'value' => $dates[0]->getTimestamp(),
485 'compare' => '>=',
486 );
487 $wp_query_args['meta_query'][] = array(
488 'key' => $key,
489 'value' => $dates[1]->getTimestamp(),
490 'compare' => '<=',
491 );
492 }
493
494 return $wp_query_args;
495 }
496
497 /**
498 * Return list of internal meta keys.
499 *
500 * @since 3.2.0
501 * @return array
502 */
503 public function get_internal_meta_keys() {
504 return $this->internal_meta_keys;
505 }
506
507 /**
508 * Check if the terms are suitable for searching.
509 *
510 * Uses an array of stopwords (terms) that are excluded from the separate
511 * term matching when searching for posts. The list of English stopwords is
512 * the approximate search engines list, and is translatable.
513 *
514 * @since 3.4.0
515 * @param array $terms Terms to check.
516 * @return array Terms that are not stopwords.
517 */
518 protected function get_valid_search_terms( $terms ) {
519 $valid_terms = array();
520 $stopwords = $this->get_search_stopwords();
521
522 foreach ( $terms as $term ) {
523 // keep before/after spaces when term is for exact match, otherwise trim quotes and spaces.
524 if ( preg_match( '/^".+"$/', $term ) ) {
525 $term = trim( $term, "\"'" );
526 } else {
527 $term = trim( $term, "\"' " );
528 }
529
530 // Avoid single A-Z and single dashes.
531 if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
532 continue;
533 }
534
535 if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) {
536 continue;
537 }
538
539 $valid_terms[] = $term;
540 }
541
542 return $valid_terms;
543 }
544
545 /**
546 * Retrieve stopwords used when parsing search terms.
547 *
548 * @since 3.4.0
549 * @return array Stopwords.
550 */
551 protected function get_search_stopwords() {
552 // Translators: This is a comma-separated list of very common words that should be excluded from a search, like a, an, and the. These are usually called "stopwords". You should not simply translate these individual words into your language. Instead, look for and provide commonly accepted stopwords in your language.
553 $stopwords = array_map(
554 'wc_strtolower',
555 array_map(
556 'trim',
557 explode(
558 ',',
559 _x(
560 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www',
561 'Comma-separated list of search stopwords in your language',
562 'woocommerce'
563 )
564 )
565 )
566 );
567
568 return apply_filters( 'wp_search_stopwords', $stopwords );
569 }
570
571 /**
572 * Get data to save to a lookup table.
573 *
574 * @since 3.6.0
575 * @param int $id ID of object to update.
576 * @param string $table Lookup table name.
577 * @return array
578 */
579 protected function get_data_for_lookup_table( $id, $table ) {
580 return array();
581 }
582
583 /**
584 * Get primary key name for lookup table.
585 *
586 * @since 3.6.0
587 * @param string $table Lookup table name.
588 * @return string
589 */
590 protected function get_primary_key_for_lookup_table( $table ) {
591 return '';
592 }
593
594 /**
595 * Update a lookup table for an object.
596 *
597 * @since 3.6.0
598 * @param int $id ID of object to update.
599 * @param string $table Lookup table name.
600 *
601 * @return NULL
602 */
603 protected function update_lookup_table( $id, $table ) {
604 global $wpdb;
605
606 $id = absint( $id );
607 $table = sanitize_key( $table );
608
609 if ( empty( $id ) || empty( $table ) ) {
610 return false;
611 }
612
613 $existing_data = wp_cache_get( 'lookup_table', 'object_' . $id );
614 $update_data = $this->get_data_for_lookup_table( $id, $table );
615
616 if ( ! empty( $update_data ) && $update_data !== $existing_data ) {
617 $wpdb->replace(
618 $wpdb->$table,
619 $update_data
620 );
621 wp_cache_set( 'lookup_table', $update_data, 'object_' . $id );
622 }
623 }
624
625 /**
626 * Delete lookup table data for an ID.
627 *
628 * @since 3.6.0
629 * @param int $id ID of object to update.
630 * @param string $table Lookup table name.
631 */
632 public function delete_from_lookup_table( $id, $table ) {
633 global $wpdb;
634
635 $id = absint( $id );
636 $table = sanitize_key( $table );
637
638 if ( empty( $id ) || empty( $table ) ) {
639 return false;
640 }
641
642 $pk = $this->get_primary_key_for_lookup_table( $table );
643
644 $wpdb->delete(
645 $wpdb->$table,
646 array(
647 $pk => $id,
648 )
649 );
650 wp_cache_delete( 'lookup_table', 'object_' . $id );
651 }
652
653 /**
654 * Converts a WP post date string into a timestamp.
655 *
656 * @since 4.8.0
657 *
658 * @param string $time_string The WP post date string.
659 * @return int|null The date string converted to a timestamp or null.
660 */
661 protected function string_to_timestamp( $time_string ) {
662 return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
663 }
664 }
665