HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/data-stores.tar
class-yith-wcwl-wishlist-data-store.php000064400000122060151540036210014212 0ustar00<?php
/**
 * Wishlist data store
 *
 * @author YITH <plugins@yithemes.com>
 * @package YITH\Wishlist\Classes\DataStores
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

if ( ! class_exists( 'YITH_WCWL_Wishlist_Data_Store' ) ) {
	/**
	 * This class implements CRUD methods for wishlists
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Data_Store {

		/**
		 * Create a new wishlist and stores it on DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to create.
		 */
		public function create( &$wishlist ) {
			global $wpdb;

			// set token if missing.
			if ( ! $wishlist->get_token() ) {
				$wishlist->set_token( $this->generate_token() );
			}

			// set slug if missing.
			$wishlist_slug = $wishlist->get_slug();

			if ( ! $wishlist_slug ) {
				$wishlist_slug = sanitize_title_with_dashes( $wishlist->get_name() );
				$wishlist->set_slug( $wishlist_slug );
			}

			// set date added if missing.
			if ( ! $wishlist->get_date_added() ) {
				$wishlist->set_date_added( gmdate( 'Y-m-d H:i:s' ) );
			}

			// set default, if needed.
			if ( $this->should_be_default() ) {
				$wishlist->set_is_default( 1 );
			}

			// set always at least an owner.
			if ( ! $wishlist->get_session_id() && ! $wishlist->get_user_id() ) {
				if ( is_user_logged_in() ) {
					$user_id = get_current_user_id();
					/**
					 * APPLY_FILTERS: yith_wcwl_add_wishlist_user_id
					 *
					 * Filter the user ID saved in the wishlist.
					 *
					 * @param int $user_id User ID
					 *
					 * @return int
					 */
					$wishlist->set_user_id( apply_filters( 'yith_wcwl_add_wishlist_user_id', $user_id ) );
				} else {
					$session_id = YITH_WCWL_Session()->get_session_id();
					/**
					 * APPLY_FILTERS: yith_wcwl_add_wishlist_session_id
					 *
					 * Filter the session ID saved in the wishlist.
					 *
					 * @param string $session_id Session ID
					 *
					 * @return string
					 */
					$wishlist->set_session_id( apply_filters( 'yith_wcwl_add_wishlist_session_id', $session_id ) );
				}
			}

			// avoid slug duplicate, adding -n to the end of the string.
			$wishlist->set_slug( $this->generate_slug( $wishlist_slug ) );

			$columns = array(
				'wishlist_privacy' => '%d',
				'wishlist_name'    => '%s',
				'wishlist_slug'    => '%s',
				'wishlist_token'   => '%s',
				'is_default'       => '%d',
			);
			$values  = array(
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_privacy
				 *
				 * Filter the wishlist privacy.
				 *
				 * @param int $privacy Wishlist privacy
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_add_wishlist_privacy', $wishlist->get_privacy() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_name
				 *
				 * Filter the wishlist name.
				 *
				 * @param string $name Wishlist name
				 *
				 * @return string
				 */
				apply_filters( 'yith_wcwl_add_wishlist_name', $wishlist->get_name() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_slug
				 *
				 * Filter the wishlist slug.
				 *
				 * @param string $slug Wishlist slug
				 *
				 * @return string
				 */
				apply_filters( 'yith_wcwl_add_wishlist_slug', $wishlist->get_slug() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_token
				 *
				 * Filter the wishlist token.
				 *
				 * @param string $token Wishlist token
				 *
				 * @return string
				 */
				apply_filters( 'yith_wcwl_add_wishlist_token', $wishlist->get_token() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_is_default
				 *
				 * Filter whether is the default wishlist.
				 *
				 * @param bool $bool Is default wishlist?
				 *
				 * @return bool
				 */
				apply_filters( 'yith_wcwl_add_wishlist_is_default', $wishlist->get_is_default() ),
			);

			$session_id = $wishlist->get_session_id();

			if ( $session_id ) {
				$columns['session_id'] = '%s';
				$values[]              = apply_filters( 'yith_wcwl_add_wishlist_session_id', $session_id );
			}

			$user_id = $wishlist->get_user_id();

			if ( $user_id ) {
				$columns['user_id'] = '%d';
				$values[]           = apply_filters( 'yith_wcwl_add_wishlist_user_id', $user_id );
			}

			$date_added = $wishlist->get_date_added( 'edit' );

			if ( $date_added ) {
				$columns['dateadded'] = 'FROM_UNIXTIME( %d )';
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_date_added
				 *
				 * Filter the date when the wishlist was created.
				 *
				 * @param int $date_added Date when the wishlist was created (timestamp)
				 *
				 * @return int
				 */
				$values[] = apply_filters( 'yith_wcwl_add_wishlist_date_added', $date_added->getTimestamp() );
			}

			$expiration = $wishlist->get_expiration( 'edit' );

			if ( $expiration ) {
				$columns['expiration'] = 'FROM_UNIXTIME( %d )';
				/**
				 * APPLY_FILTERS: yith_wcwl_add_wishlist_expiration
				 *
				 * Filter the date when the wishlist will expire.
				 *
				 * @param int $expiration_date Date when the wishlist will expire (timestamp)
				 *
				 * @return int
				 */
				$values[] = apply_filters( 'yith_wcwl_add_wishlist_expiration', $expiration->getTimestamp() );
			}

			// if session wishlist, set always an expiration.
			$session_expiration = YITH_WCWL_Session()->get_session_expiration();

			if ( isset( $columns['session_id'] ) && ! $expiration && $session_expiration ) {
				$columns['expiration'] = 'FROM_UNIXTIME( %d )';
				$values[]              = apply_filters( 'yith_wcwl_add_wishlist_expiration', $session_expiration );
			}

			$query_columns = implode( ', ', array_map( 'esc_sql', array_keys( $columns ) ) );
			$query_values  = implode( ', ', array_values( $columns ) );
			$query         = "INSERT INTO {$wpdb->yith_wcwl_wishlists} ( {$query_columns} ) VALUES ( {$query_values} ) ";

			do_action( 'yith_wcwl_before_create_wishlist_res_query', $query, $values );

			$res = $wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( $res ) {
				/**
				 * APPLY_FILTERS: yith_wcwl_wishlist_correctly_created
				 *
				 * Filter the ID of the Wishlist created.
				 *
				 * @param int $id Wishlist ID
				 *
				 * @return int
				 */
				$id = apply_filters( 'yith_wcwl_wishlist_correctly_created', intval( $wpdb->insert_id ) );

				$wishlist->set_id( $id );
				$wishlist->apply_changes();
				$this->clear_caches( $wishlist );

				/**
				 * DO_ACTION: yith_wcwl_new_wishlist
				 *
				 * Allows to fire some action when a new wishlist is created.
				 *
				 * @param int                $wishlist_id   Wishlist ID
				 * @param YITH_WCWL_Wishlist $wishlist_data Wishlist object
				 */
				do_action( 'yith_wcwl_new_wishlist', $wishlist->get_id(), $wishlist );
			}
		}

		/**
		 * Read data from DB for a specific wishlist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @throws Exception When cannot retrieve specified wishlist.
		 */
		public function read( &$wishlist ) {
			global $wpdb;

			$wishlist->set_defaults();

			$id    = $wishlist->get_id();
			$token = $wishlist->get_token();

			if ( ! $id && ! $token ) {
				throw new Exception( esc_html__( 'Invalid wishlist.', 'yith-woocommerce-wishlist' ) );
			}

			$wishlist_data = $wishlist->get_id() ? wp_cache_get( 'wishlist-id-' . $wishlist->get_id(), 'wishlists' ) : wp_cache_get( 'wishlist-token-' . $wishlist->get_token(), 'wishlists' );

			if ( ! $wishlist_data ) {
				// format query to retrieve wishlist.
				$query = false;
				if ( $id ) {
					$query = $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_wishlists} WHERE ID = %d", $id );
				} elseif ( $token ) {
					$query = $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_wishlists} WHERE wishlist_token = %s", $token );
				}

				// retrieve wishlist data.
				$wishlist_data = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

				wp_cache_set( 'wishlist-id-' . $wishlist->get_id(), $wishlist_data, 'wishlists' );
				wp_cache_set( 'wishlist-token-' . $wishlist->get_token(), $wishlist_data, 'wishlists' );
			}

			if ( ! $wishlist_data ) {
				throw new Exception( esc_html__( 'Invalid wishlist.', 'yith-woocommerce-wishlist' ) );
			}

			// set wishlist props.
			$wishlist->set_props(
				array(
					'id'         => $wishlist_data->ID,
					'privacy'    => $wishlist_data->wishlist_privacy,
					'user_id'    => $wishlist_data->user_id,
					'session_id' => isset( $wishlist_data->session_id ) ? $wishlist_data->session_id : '',
					'name'       => wc_clean( stripslashes( $wishlist_data->wishlist_name ) ),
					'slug'       => $wishlist_data->wishlist_slug,
					'token'      => $wishlist_data->wishlist_token,
					'is_default' => $wishlist_data->is_default,
					'date_added' => $wishlist_data->dateadded,
					'expiration' => isset( $wishlist_data->expiration ) ? $wishlist_data->expiration : '',
				)
			);
			$wishlist->set_object_read( true );
		}

		/**
		 * Update wishlist data on DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to save on db, with $changes property.
		 */
		public function update( &$wishlist ) {
			global $wpdb;

			if ( ! $wishlist->get_id() ) {
				return;
			}

			$data    = $wishlist->get_data();
			$changes = $wishlist->get_changes();

			if ( array_intersect( array( 'user_id', 'session_id', 'slug', 'name', 'token', 'privacy', 'expiration', 'date_added', 'is_default' ), array_keys( $changes ) ) ) {
				$columns = array(
					'wishlist_privacy' => '%d',
					'wishlist_name'    => '%s',
					'wishlist_token'   => '%s',
					'is_default'       => '%d',
					'dateadded'        => 'FROM_UNIXTIME( %d )',
				);
				$values  = array(
					$wishlist->get_privacy(),
					$wishlist->get_name(),
					$wishlist->get_token(),
					$wishlist->get_is_default(),
					$wishlist->get_date_added( 'edit' ) ? $wishlist->get_date_added( 'edit' )->getTimestamp() : time(),
				);

				$session_id = $wishlist->get_session_id();

				if ( $session_id ) {
					$columns['session_id'] = '%s';
					/**
					 * APPLY_FILTERS: yith_wcwl_update_wishlist_session_id
					 *
					 * Filter the session ID of the updated wishlist.
					 *
					 * @param string $session_id Session ID
					 *
					 * @return string
					 */
					$values[] = apply_filters( 'yith_wcwl_update_wishlist_session_id', $session_id );
				} else {
					$columns['session_id'] = 'NULL';
				}

				$user_id = $wishlist->get_user_id();

				if ( $user_id ) {
					$columns['user_id'] = '%d';
					/**
					 * APPLY_FILTERS: yith_wcwl_update_wishlist_user_id
					 *
					 * Filter the user ID of the updated wishlist.
					 *
					 * @param int $user_id User ID
					 *
					 * @return int
					 */
					$values[] = apply_filters( 'yith_wcwl_update_wishlist_user_id', $user_id );
				} else {
					$columns['user_id'] = 'NULL';
				}

				$expiration = $wishlist->get_expiration( 'edit' );

				if ( $expiration ) {
					$columns['expiration'] = 'FROM_UNIXTIME( %d )';
					/**
					 * APPLY_FILTERS: yith_wcwl_update_wishlist_expiration
					 *
					 * Filter the expiration date of the updated wishlist.
					 *
					 * @param int $expiration_date Date when the wishlist will expire (timestamp)
					 *
					 * @return int
					 */
					$values[] = apply_filters( 'yith_wcwl_update_wishlist_expiration', $expiration->getTimestamp() );
				} else {
					$columns['expiration'] = 'NULL';
				}

				$wishlist_slug = $wishlist->get_slug();

				if ( isset( $changes['slug'] ) && $wishlist_slug !== $data['slug'] ) {
					$columns['wishlist_slug'] = '%s';
					$values[]                 = $this->generate_slug( $wishlist_slug );
				}

				$this->update_raw( $columns, $values, array( 'ID' => '%d' ), array( $wishlist->get_id() ) );
			}

			$wishlist->apply_changes();
			$this->clear_caches( $wishlist );

			/**
			 * DO_ACTION: yith_wcwl_update_wishlist
			 *
			 * Allows to fire some action when a wishlist is updated.
			 *
			 * @param int                $wishlist_id   Wishlist ID
			 * @param YITH_WCWL_Wishlist $wishlist_data Wishlist object
			 */
			do_action( 'yith_wcwl_update_wishlist', $wishlist->get_id(), $wishlist );
		}

		/**
		 * Delete a wishlist from DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to delete.
		 */
		public function delete( &$wishlist ) {
			global $wpdb;

			$id         = $wishlist->get_id();
			$is_default = $wishlist->is_default();
			$user_id    = $wishlist->get_user_id();
			$session_id = $wishlist->get_session_id();

			if ( ! $id ) {
				return;
			}

			/**
			 * DO_ACTION: yith_wcwl_before_delete_wishlist
			 *
			 * Allows to fire some action before a wishlist is deleted.
			 *
			 * @param int $wishlist_id Wishlist ID
			 */
			do_action( 'yith_wcwl_before_delete_wishlist', $wishlist->get_id() );

			$this->clear_caches( $wishlist );

			// delete wishlist and all its items.
			$wpdb->delete( $wpdb->yith_wcwl_items, array( 'wishlist_id' => $id ) );
			$wpdb->delete( $wpdb->yith_wcwl_wishlists, array( 'ID' => $id ) );

			/**
			 * DO_ACTION: yith_wcwl_delete_wishlist
			 *
			 * Allows to fire some action when a wishlist is deleted.
			 *
			 * @param int $wishlist_id Wishlist ID
			 */
			do_action( 'yith_wcwl_delete_wishlist', $wishlist->get_id() );

			$wishlist->set_id( 0 );

			/**
			 * DO_ACTION: yith_wcwl_deleted_wishlist
			 *
			 * Allows to fire some action after a wishlist is deleted.
			 *
			 * @param int $id Wishlist ID
			 */
			do_action( 'yith_wcwl_deleted_wishlist', $id );

			if ( $is_default && ( $user_id || $session_id ) ) {
				// retrieve other lists for the same user.
				$other_lists = $this->query(
					array_merge(
						array(
							'orderby' => 'dateadded',
							'order'   => 'asc',
						),
						$user_id ? array( 'user_id' => $user_id ) : array(),
						$session_id ? array( 'session_id' => $session_id ) : array()
					)
				);

				if ( ! empty( $other_lists ) ) {
					$new_default = $other_lists[0];

					$new_default->set_is_default( 1 );
					$new_default->save();
				}
			}
		}

		/**
		 * Delete expired session wishlist from DB
		 *
		 * @return void
		 */
		public function delete_expired() {
			global $wpdb;

			$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_items} WHERE wishlist_id IN ( SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE expiration < NOW() and user_id IS NULL )" );
			$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_wishlists} WHERE expiration < NOW() and user_id IS NULL" );
		}

		/**
		 * Query database to search
		 *
		 * @param array $args Array of parameters used for the query:<br/>
		 * [<br/>
		 *   'id'                   // Wishlist id<br/>
		 *   'user_id'              // User id<br/>
		 *   'session_id'           // Session id<br/>
		 *   'wishlist_slug'        // Wishlist slug, exact match<br/>
		 *   'wishlist_name'        // Wishlist name, like<br/>
		 *   'wishlist_token'       // Wishlist token, exact match<br/>
		 *   'wishlist_visibility'  // all, visible, public, shared, private<br/>
		 *   'user_search'          // String to search within user fields<br/>
		 *   's'                    // String to search within wishlist fields<br/>
		 *   'is_default'           // Whether searched wishlist is default<br/>
		 *   'orderby'              // Any of the table columns<br/>
		 *   'order'                // ASC, DESC<br/>
		 *   'limit'                // Limit of items to retrieve<br/>
		 *   'offset'               // Offset of items to retrieve<br/>
		 *   'show_empty'           // Whether to show empty wishlists<br/>
		 * ].
		 *
		 * @return \YITH_WCWL_Wishlist[] Array of matched wishlists.
		 */
		public function query( $args = array() ) {
			global $wpdb;

			$default = array(
				'id'                  => false,
				'user_id'             => ( is_user_logged_in() ) ? get_current_user_id() : false,
				'session_id'          => ( ! is_user_logged_in() ) ? YITH_WCWL_Session()->maybe_get_session_id() : false,
				'wishlist_slug'       => false,
				'wishlist_name'       => false,
				'wishlist_token'      => false,
				/**
				 * APPLY_FILTERS: yith_wcwl_wishlist_visibility_string_value
				 *
				 * Filter the wishlist visibility value.
				 *
				 * @param string $wishlist_visibility Wishlist visibility. Possible values are: all | visible | public | shared | private
				 *
				 * @return string
				 */
				'wishlist_visibility' => apply_filters( 'yith_wcwl_wishlist_visibility_string_value', 'all' ), // all | visible | public | shared | private.
				'user_search'         => false,
				's'                   => false,
				'is_default'          => false,
				'orderby'             => '',
				'order'               => 'DESC',
				'limit'               => false,
				'offset'              => 0,
				'show_empty'          => true,
			);

			// if there is no current wishlist, and user was asking for current one, short-circuit query, as pointless.
			if ( ! is_user_logged_in() && ! YITH_WCWL_Session()->has_session() && ! isset( $args['user_id'] ) && ! isset( $args['session_id'] ) ) {
				return array();
			}

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql  = 'SELECT SQL_CALC_FOUND_ROWS l.ID';
			$sql .= " FROM `{$wpdb->yith_wcwl_wishlists}` AS l";

			if ( ! empty( $user_search ) || ! empty( $s ) || ( ! empty( $orderby ) && 'user_login' === $orderby ) ) {
				$sql .= " LEFT JOIN `{$wpdb->users}` AS u ON l.`user_id` = u.ID";
			}

			if ( ! empty( $user_search ) || ! empty( $s ) ) {
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS umn ON umn.`user_id` = u.`ID`";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS ums ON ums.`user_id` = u.`ID`";
			}

			$sql     .= ' WHERE 1';
			$sql_args = array();

			if ( ! empty( $user_id ) ) {
				$sql .= ' AND l.`user_id` = %d';

				$sql_args[] = $user_id;
			}

			if ( ! empty( $session_id ) ) {
				$sql .= ' AND l.`session_id` = %s AND l.`expiration` > NOW()';

				$sql_args[] = $session_id;
			}

			if ( ! empty( $user_search ) && empty( $s ) ) {
				$sql .= ' AND (
							umn.`meta_key` = %s AND
							ums.`meta_key` = %s AND
							(
								u.`user_email` LIKE %s OR
								umn.`meta_value` LIKE %s OR
								ums.`meta_value` LIKE %s
							)
						)';

				$search_value = '%' . esc_sql( $user_search ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
			}

			if ( ! empty( $s ) ) {
				$sql .= ' AND ( 
							( 
								umn.`meta_key` = %s AND 
								ums.`meta_key` = %s AND 
								( 
									u.`user_email` LIKE %s OR
									u.`user_login` LIKE %s OR
									umn.`meta_value` LIKE %s OR
									ums.`meta_value` LIKE %s
								) 
							) OR 
							l.wishlist_name LIKE %s OR 
							l.wishlist_slug LIKE %s OR 
							l.wishlist_token LIKE %s 
						)';

				$search_value = '%' . esc_sql( $s ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
			}

			if ( ! empty( $is_default ) ) {
				$sql       .= ' AND l.`is_default` = %d';
				$sql_args[] = $is_default;
			}

			if ( ! empty( $id ) ) {
				$sql       .= ' AND l.`ID` = %d';
				$sql_args[] = $id;
			}

			if ( isset( $wishlist_slug ) && false !== $wishlist_slug ) {
				$sql       .= ' AND l.`wishlist_slug` = %s';
				$sql_args[] = sanitize_title_with_dashes( $wishlist_slug );
			}

			if ( ! empty( $wishlist_token ) ) {
				$sql       .= ' AND l.`wishlist_token` = %s';
				$sql_args[] = $wishlist_token;
			}

			if ( ! empty( $wishlist_name ) ) {
				$sql       .= ' AND l.`wishlist_name` LIKE %s';
				$sql_args[] = '%' . esc_sql( $wishlist_name ) . '%';
			}

			if ( isset( $wishlist_visibility ) && 'all' !== $wishlist_visibility ) {
				if ( ! is_int( $wishlist_visibility ) ) {
					$wishlist_visibility = yith_wcwl_get_privacy_value( $wishlist_visibility );
				}

				$sql       .= ' AND l.`wishlist_privacy` = %d';
				$sql_args[] = $wishlist_visibility;
			}

			if ( empty( $show_empty ) ) {
				$sql .= " AND l.`ID` IN ( SELECT wishlist_id FROM {$wpdb->yith_wcwl_items} )";
			}

			$sql .= ' GROUP BY l.ID';
			$sql .= ' ORDER BY';

			if ( ! empty( $orderby ) && isset( $order ) ) {
				$sql .= ' ' . esc_sql( $orderby ) . ' ' . esc_sql( $order ) . ', ';
			}

			$sql .= ' is_default DESC';

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql       .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			if ( ! empty( $sql_args ) ) {
				$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			$lists = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( ! empty( $lists ) ) {
				$lists = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist' ), $lists );
			} else {
				$lists = array();
			}

			/**
			 * APPLY_FILTERS: yith_wcwl_get_wishlists
			 *
			 * Filter the wishlists retrieved in the query.
			 *
			 * @param array $lists Array of wishlists
			 * @param array $args  Array of arguments
			 *
			 * @return array
			 */
			return apply_filters( 'yith_wcwl_get_wishlists', $lists, $args );
		}

		/**
		 * Counts items that matches
		 *
		 * @param array $args Same parameters allowed for {@see query} method.
		 * @return int Count of items
		 */
		public function count( $args = array() ) {
			// retrieve number of items found.
			return count( $this->query( $args ) );
		}

		/**
		 * Search user ids whose wishlists match passed parameters
		 * NOTE: this will only retrieve wishlists for a logged in user, while guests wishlist will be ignored
		 *
		 * @param mixed $args Array of valid arguments<br/>
		 * [<br/>
		 *     'search' // String to match against first name / last name / user login or user email of wishlist owner<br/>
		 *     'limit'  // Pagination param: number of items to show in one page. 0 to show all items<br/>
		 *     'offset' // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 * ].
		 * @return int[] Array of user ids
		 */
		public function search_users( $args = array() ) {
			global $wpdb;

			$default = array(
				'search' => false,
				'limit'  => false,
				'offset' => 0,
			);

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT DISTINCT i.user_id
                    FROM {$wpdb->yith_wcwl_items} AS i
                    LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON i.wishlist_id = l.ID";

			if ( ! empty( $search ) ) {
				$sql .= " LEFT JOIN `{$wpdb->users}` AS u ON l.`user_id` = u.ID";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS umn ON umn.`user_id` = u.`ID`";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS ums ON ums.`user_id` = u.`ID`";
			}

			$sql     .= ' WHERE l.wishlist_privacy = %d';
			$sql_args = array( 0 );

			if ( ! empty( $search ) ) {
				$sql .= ' AND ( 
							umn.`meta_key` = %s AND 
							ums.`meta_key` = %s AND 
							( 
								u.`user_email` LIKE %s OR 
								u.`user_login` LIKE %s OR 
								umn.`meta_value` LIKE %s OR 
								ums.`meta_value` LIKE %s
							)
						)';

				$search_string = '%' . esc_sql( $search ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql .= " LIMIT {$offset}, {$limit}";
			}

			$res = $wpdb->get_col( $wpdb->prepare( $sql, $sql_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			return $res;
		}

		/**
		 * Raw update method; useful when it is needed to update a bunch of wishlists
		 *
		 * @param array $columns Array of columns to update, in the following format: 'column_id' => 'column_type'.
		 * @param array $column_values Array of values to apply to the query; must have same number of elements of columns, and they must respect defined tpe.
		 * @param array $conditions Array of where conditions, in the following format: 'column_id' => 'columns_type'.
		 * @param array $conditions_values Array of values to apply to where condition; must have same number of elements of columns, and they must respect defined tpe.
		 * @param bool  $clear_caches Whether to clear stored value before operation or not.
		 * @pram $clear_caches bool Whether system should clear caches (this is optional since other methods may want to run more optimized clear)
		 *
		 * @return void
		 */
		public function update_raw( $columns, $column_values, $conditions = array(), $conditions_values = array(), $clear_caches = false ) {
			global $wpdb;

			// calculate where statement.
			$query_where = '';

			if ( ! empty( $conditions ) ) {
				$query_where = array();

				foreach ( $conditions as $column => $value ) {
					$query_where[] = $column . '=' . $value;
				}

				$query_where = ' WHERE ' . implode( ' AND ', $query_where );
			}

			// retrieves wishlists that will be affected by the changes.
			if ( $clear_caches ) {
				$query = "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} {$query_where}";
				$query = $conditions ? $wpdb->prepare( $query, $conditions_values ) : $query; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$ids   = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// calculate set statement.
			$query_columns = array();

			foreach ( $columns as $column => $value ) {
				$query_columns[] = $column . '=' . $value;
			}

			$query_columns = implode( ', ', $query_columns );

			// build query, and execute it.
			$query  = "UPDATE {$wpdb->yith_wcwl_wishlists} SET {$query_columns} {$query_where}";
			$values = $conditions ? array_merge( $column_values, $conditions_values ) : $column_values;

			$wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			// clear cache for updated items.
			if ( $clear_caches && $ids ) {
				foreach ( $ids as $id ) {
					$this->clear_caches( $id );
				}
			}
		}

		/**
		 * Retrieve all items for the wishlist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @return \YITH_WCWL_Wishlist_Item[] Array or Wishlist items for the wishlist
		 */
		public function read_items( $wishlist ) {
			global $wpdb;

			// Get from cache if available.
			$items = 0 < $wishlist->get_id() ? wp_cache_get( 'wishlist-items-' . $wishlist->get_id(), 'wishlists' ) : false;

			if ( false === $items ) {
				$query = "SELECT i.* FROM {$wpdb->yith_wcwl_items} as i INNER JOIN {$wpdb->posts} as p on i.prod_id = p.ID WHERE wishlist_id = %d AND p.post_type IN ( %s, %s ) AND p.post_status = %s";

				// remove hidden products from result.
				$hidden_products = yith_wcwl_get_hidden_products();

				/**
				 * APPLY_FILTERS: yith_wcwl_remove_hidden_products_via_query
				 *
				 * Filter whether to remove hidden products via query.
				 *
				 * @param bool $bool Remove hidden products or not?
				 *
				 * @return bool
				 */
				if ( ! empty( $hidden_products ) && apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
					$query .= ' AND prod_id NOT IN ( ' . implode( ', ', array_filter( $hidden_products, 'esc_sql' ) ) . ' )';
				}

				// order by statement.
				/**
				 * APPLY_FILTERS: yith_wcwl_wishlist_items_sorting
				 *
				 * Filter the order arguments for the query.
				 *
				 * @param array $args Array of order arguments
				 *
				 * @return array
				 */
				$orders  = apply_filters(
					'yith_wcwl_wishlist_items_sorting',
					array(
						'position' => 'ASC',
						'ID'       => 'DESC',
					)
				);
				$orderby = implode(
					', ',
					array_map(
						function ( $column, $sorting ) {
							return "$column $sorting";
						},
						array_keys( $orders ),
						$orders
					)
				);

				$query .= ' ORDER BY ' . esc_sql( $orderby );

				$items = $wpdb->get_results(
					$wpdb->prepare(
						$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
						array(
							$wishlist->get_id(),
							'product',
							'product_variation',
							'publish',
						)
					)
				);

				/**
				 * This filter was added to allow developer remove hidden products using a foreach loop, instead of the query
				 * It is required when the store contains a huge number of hidden products, and the resulting query would fail
				 * to be submitted to DBMS because of its size
				 *
				 * This code requires reasonable amount of products in the wishlist
				 * A great number of products retrieved from the main query could easily degrade performance of the overall system
				 *
				 * @since 3.0.7
				 */
				if ( ! empty( $hidden_products ) && ! empty( $items ) && ! apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
					foreach ( $items as $item_id => $item ) {
						if ( ! in_array( $item->prod_id, $hidden_products, true ) ) {
							continue;
						}

						unset( $items[ $item_id ] );
					}
				}

				foreach ( $items as $item ) {
					wp_cache_set( 'item-' . $item->ID, $item, 'wishlist-items' );
				}

				if ( 0 < $wishlist->get_id() ) {
					wp_cache_set( 'wishlist-items-' . $wishlist->get_id(), $items, 'wishlists' );
				}
			}

			if ( ! empty( $items ) ) {
				$items = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist_item' ), array_combine( wp_list_pluck( $items, 'prod_id' ), $items ) );
			} else {
				$items = array();
			}

			/**
			 * APPLY_FILTERS: yith_wcwl_get_products
			 *
			 * Filter the products retrieved from the wishlist.
			 *
			 * @param array $items Array of products
			 * @param array $args  Array of arguments
			 *
			 * @return array
			 */
			return apply_filters( 'yith_wcwl_get_products', $items, array( 'wishlist_id' => $wishlist->get_id() ) );
		}

		/**
		 * Delete all items from the wishist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @return void
		 */
		public function delete_items( $wishlist ) {
			global $wpdb;

			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->yith_wcwl_items} WHERE wishlist_id = %d", $wishlist->get_id() ) );

			$this->clear_caches( $wishlist );
		}

		/**
		 * Generate default token for the wishlist
		 *
		 * @return string Wishlist token
		 */
		public function generate_token() {
			global $wpdb;

			$sql = "SELECT COUNT(*) FROM `{$wpdb->yith_wcwl_wishlists}` WHERE `wishlist_token` = %s";

			do {
				$dictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
				$nchars     = 12;
				$token      = '';

				for ( $i = 0; $i <= $nchars - 1; $i++ ) {
					$token .= $dictionary[ wp_rand( 0, strlen( $dictionary ) - 1 ) ];
				}

				$count = $wpdb->get_var( $wpdb->prepare( $sql, $token ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			} while ( $count );

			return $token;
		}

		/**
		 * When a session is finalized, all session wishlists will be converted to user wishlists
		 * This method takes also care of allowing just one default per time after finalization
		 *
		 * @param string $session_id Session id.
		 * @param int    $user_id User id.
		 *
		 * @return void
		 */
		public function assign_to_user( $session_id, $user_id ) {
			global $wpdb;

			// update any item that is assigned to the list.
			$items = $wpdb->get_col( $wpdb->prepare( "SELECT i.ID FROM {$wpdb->yith_wcwl_items} AS i LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON l.ID = i.wishlist_id WHERE l.session_id = %s", $session_id ) );

			if ( ! empty( $items ) ) {
				$items_string = implode( ',', array_map( 'esc_sql', $items ) );
				$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_items} SET user_id = %d WHERE ID IN ({$items_string})", $user_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}

			// set user id for any session wishlist, and remove session data.
			$this->update_raw(
				array(
					'session_id' => 'NULL',
					'expiration' => 'NULL',
					'user_id'    => '%d',
				),
				array( $user_id ),
				array( 'session_id' => '%s' ),
				array( $session_id )
			);

			// retrieves default wishlist ids.
			$default_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE is_default = %d AND user_id = %d ORDER BY dateadded ASC", 1, $user_id ) );

			// if we find more than one default list, fix data in db.
			if ( count( $default_ids ) > 1 ) {

				// search for master default wishlist.
				$master_default_wishlist = array_shift( $default_ids );
				$where_statement         = implode( ', ', array_map( 'esc_sql', $default_ids ) );

				try {
					/**
					 * APPLY_FILTERS: yith_wcwl_merge_default_wishlists
					 *
					 * Filter whether merge all default wishlists into the oldest one.
					 *
					 * @param bool $bool Merge all default wishlists or not?
					 *
					 * @return bool
					 */
					if ( apply_filters( 'yith_wcwl_merge_default_wishlists', true ) ) {
						// by default we merge all default wishlists into oldest one (master default wishlist).

						// change wishlist id to master default id.
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_items} SET wishlist_id = %d WHERE wishlist_id IN ({$where_statement})", $master_default_wishlist ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

						// delete slave default wishlists.
						$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_wishlists} WHERE ID IN ({$where_statement})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					} else {
						// otherwise, we just leave all the wishlists as they are, but we remove default flag from latest.

						// remove default flag.
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_wishlists} SET is_default = %d WHERE ID IN ({$where_statement})", 0 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

						// set name where it is missing.
						/**
						 * APPLY_FILTERS: yith_wcwl_default_wishlist_formatted_title
						 *
						 * Filter the default wishlist title.
						 *
						 * @param string $title Default wishlist title
						 *
						 * @return string
						 */
						$default_title = apply_filters( 'yith_wcwl_default_wishlist_formatted_title', get_option( 'yith_wcwl_wishlist_title', __( 'My wishlist', 'yith-woocommerce-wishlist' ) ) );
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_wishlists} SET wishlist_name = %s WHERE ID IN ({$where_statement}) AND wishlist_name = ''", $default_title ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					}
				} catch ( Exception $e ) {
					return;
				}
			}
		}

		/**
		 * Retrieve default wishlist for current user/session; if none is found, generate it
		 *
		 * @param string|int|bool $id       Pass this param when you want to retrieve a wishlist for a specific user/session.
		 * @param string          $context  Context; when on edit context, wishlist will be created, if not exists.
		 * @return \YITH_WCWL_Wishlist|bool Default wishlist for current user/session, or false on failure
		 */
		public function get_default_wishlist( $id = false, $context = 'read' ) {
			global $wpdb;

			$wishlist_id = false;
			$cache_key   = false;

			$user_id    = get_current_user_id();
			$session_id = YITH_WCWL_Session()->maybe_get_session_id();

			if ( ! empty( $id ) && is_int( $id ) ) {
				$cache_key   = 'wishlist-default-' . $id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = 1", $id ) );
			} elseif ( ! empty( $id ) && is_string( $id ) ) {
				$cache_key   = 'wishlist-default-' . $id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = 1", $id ) );
			} elseif ( $user_id ) {
				$cache_key   = 'wishlist-default-' . $user_id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = 1", $user_id ) );
			} elseif ( $session_id ) {
				$cache_key   = 'wishlist-default-' . $session_id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = 1", $session_id ) );
			}

			if ( $wishlist_id ) {
				if ( $cache_key ) {
					wp_cache_set( $cache_key, $wishlist_id, 'wishlists' );
				}

				return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
			} elseif ( 'edit' === $context ) {
				$wishlist = $this->generate_default_wishlist( $id );

				if ( $cache_key ) {
					wp_cache_set( $cache_key, $wishlist->get_id(), 'wishlists' );
				}

				return $wishlist;
			} else {
				/**
				 * If no default wishlist was found, register null as cache value
				 * This will be used until someone tries to edit the list (entering previous elseif),
				 * causing a new default wishlist to be automatically generated and stored in cache, replacing null
				 *
				 * @since 3.0.6
				 */
				if ( $cache_key ) {
					wp_cache_set( $cache_key, null, 'wishlists' );
				}

				return false;
			}
		}

		/**
		 * Generate a new default wishlist
		 *
		 * @param string|int|bool $id Pass this param when you want to create a wishlist for a specific user/session.
		 * @return YITH_WCWL_Wishlist|bool Brand new default wishlist, or false on failure
		 */
		public function generate_default_wishlist( $id ) {
			try {
				$default_wishlist = new YITH_WCWL_Wishlist();

				if ( ! empty( $id ) && is_int( $id ) ) {
					$default_wishlist->set_user_id( $id );
				} elseif ( ! empty( $id ) && is_string( $id ) ) {
					$default_wishlist->set_session_id( $id );
				}

				$default_wishlist->save();

				/**
				 * Let developers perform processing when default wishlist is created
				 *
				 * @since 3.0.10
				 */
				/**
				 * DO_ACTION: yith_wcwl_generated_default_wishlist
				 *
				 * Allows to fire some action when default wishlist is created.
				 *
				 * @param YITH_WCWL_Wishlist $default_wishlist Default wishlist object
				 * @param int                $id               Wishlist ID
				 */
				do_action( 'yith_wcwl_generated_default_wishlist', $default_wishlist, $id );
			} catch ( Exception $e ) {
				return false;
			}

			return $default_wishlist;
		}

		/**
		 * Generate unique slug for the wishlisst
		 *
		 * @param string $slug Original slug assigned to the wishlist (it cuold be custom assigned, or generated from the title).
		 * @return string Unique slug, derived from original one adding ordinal number when necessary
		 */
		public function generate_slug( $slug ) {
			if ( empty( $slug ) ) {
				return '';
			}

			while ( $this->slug_exists( $slug ) ) {
				$match = array();

				if ( ! preg_match( '/([a-z-]+)-([0-9]+)/', $slug, $match ) ) {
					$i = 2;
				} else {
					$i    = intval( $match[2] ) + 1;
					$slug = $match[1];
				}

				$suffix = '-' . $i;
				$slug   = substr( $slug, 0, 200 - strlen( $suffix ) ) . $suffix;
			}

			return $slug;
		}

		/**
		 * Checks if a slug already exists
		 *
		 * @param string $slug Slug to check on db.
		 *
		 * @return bool Whether slug already exists for current session or not
		 */
		public function slug_exists( $slug ) {
			global $wpdb;

			$res = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->yith_wcwl_wishlists} WHERE wishlist_slug = %s", $slug ) );

			return (bool) $res;
		}

		/**
		 * Check if we're registering first wishlist for the user/session
		 *
		 * @return bool Whether current wishlist should be default
		 */
		protected function should_be_default() {
			global $wpdb;

			$user_id     = get_current_user_id();
			$customer_id = YITH_WCWL_Session()->maybe_get_session_id();

			if ( $user_id ) {
				$wishlists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( ID ) FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = %d", $user_id, 1 ) );

				return ! (bool) $wishlists;
			}

			if ( $customer_id ) {
				$wishlists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( ID ) FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = %d", $customer_id, 1 ) );

				return ! (bool) $wishlists;
			}

			return true;
		}

		/**
		 * Clear wishlist related caches
		 *
		 * @param \YITH_WCWL_Wishlist|int|string $wishlist Wishlist object.
		 * @return void
		 */
		protected function clear_caches( &$wishlist ) {
			if ( $wishlist instanceof YITH_WCWL_Wishlist ) {
				$id    = $wishlist->get_id();
				$token = $wishlist->get_token();
			} elseif ( intval( $wishlist ) ) {
				$id       = $wishlist;
				$wishlist = yith_wcwl_get_wishlist( $wishlist );
				$token    = $wishlist ? $wishlist->get_token() : false;
			} else {
				$token    = $wishlist;
				$wishlist = yith_wcwl_get_wishlist( $wishlist );
				$id       = $wishlist ? $wishlist->get_id() : false;
			}

			$user_id    = $wishlist ? $wishlist->get_user_id() : false;
			$session_id = $wishlist ? $wishlist->get_session_id() : false;

			wp_cache_delete( 'wishlist-items-' . $id, 'wishlists' );
			wp_cache_delete( 'wishlist-id-' . $id, 'wishlists' );
			wp_cache_delete( 'wishlist-token-' . $token, 'wishlists' );

			if ( $user_id ) {
				wp_cache_delete( 'user-wishlists-' . $user_id, 'wishlists' );
			}

			if ( $session_id ) {
				wp_cache_delete( 'user-wishlists-' . $session_id, 'wishlists' );
			}
		}
	}
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
class-yith-wcwl-wishlist-item-data-store.php000064400000064453151540036210015161 0ustar00<?php
/**
 * Wishlist data store
 *
 * @package YITH\Wishlist\Classes\DataStores
 * @author  YITH <plugins@yithemes.com>
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

if ( ! class_exists( 'YITH_WCWL_Wishlist_Item_Data_Store' ) ) {
	/**
	 * This class implements CRUD methods for wishlists' items
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Item_Data_Store extends WC_Data_Store_WP {

		protected $meta_type = 'yith_wcwl_item';

		/**
		 * Create a new wishlist item in the database.
		 *
		 * @param \YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 * @since 3.0.0
		 */
		public function create( &$item ) {
			global $wpdb;

			$product_id  = $item->get_original_product_id();
			$wishlist_id = $item->get_wishlist_id();

			if ( ! $product_id || ! $wishlist_id ) {
				return;
			}

			$item_id = YITH_WCWL_Wishlist_Factory::get_wishlist_item_by_product_id( $wishlist_id, $product_id );

			if ( $item_id ) {
				$item->set_id( $item_id );

				$this->update( $item );
				return;
			}

			$columns = array(
				'prod_id'           => '%d',
				'quantity'          => '%d',
				'wishlist_id'       => '%d',
				'position'          => '%d',
				'original_price'    => '%f',
				'original_currency' => '%s',
				'on_sale'           => '%d',
			);
			$values  = array(
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_product_id
				 *
				 * Filter the ID of the product added to the wishlist.
				 *
				 * @param int $product_id Product ID
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_product_id', $product_id ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_quantity
				 *
				 * Filter the quantity of the product added to the wishlist.
				 *
				 * @param int $quantity   Product quantity
				 * @param int $product_id Product ID
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_quantity', $item->get_quantity(), $product_id ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_wishlist_id
				 *
				 * Filter the wishlist ID where the products are added to.
				 *
				 * @param int $wishlist_id Wishlist ID
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_id', $wishlist_id ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_position
				 *
				 * Filter the position where the product will be added to the wishlist.
				 *
				 * @param int $item_position Item position in the wishlist
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_position', $item->get_position() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_original_price
				 *
				 * Filter the price of the product added to the wishlist.
				 *
				 * @param int $product_price Product price
				 *
				 * @return int
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_original_price', $item->get_product_price() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_original_currency
				 *
				 * Filter the currency of the product added to the wishlist.
				 *
				 * @param string $currency Currency
				 *
				 * @return string
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_original_currency', $item->get_original_currency() ),
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_on_sale
				 *
				 * Filter whether the product added to the wishlist is on sale.
				 *
				 * @param bool $bool Is the product on sale or not?
				 *
				 * @return bool
				 */
				apply_filters( 'yith_wcwl_adding_to_wishlist_on_sale', $item->is_on_sale() ),
			);

			$user_id = $item->get_user_id();

			if ( $user_id ) {
				$columns['user_id'] = '%d';
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_user_id
				 *
				 * Filter the user ID saved in the wishlist.
				 *
				 * @param int $user_id User ID
				 *
				 * @return int
				 */
				$values[] = apply_filters( 'yith_wcwl_adding_to_wishlist_user_id', $user_id );
			}

			$date_added = $item->get_date_added( 'edit' );

			if ( $date_added ) {
				$columns['dateadded'] = 'FROM_UNIXTIME( %d )';
				/**
				 * APPLY_FILTERS: yith_wcwl_adding_to_wishlist_date_added
				 *
				 * Filter the date when the wishlist was created.
				 *
				 * @param int $date_added Date when the wishlist was created (timestamp)
				 *
				 * @return int
				 */
				$values[] = apply_filters( 'yith_wcwl_adding_to_wishlist_date_added', $date_added->getTimestamp() );
			}

			$query_columns = implode( ', ', array_map( 'esc_sql', array_keys( $columns ) ) );
			$query_values  = implode( ', ', array_values( $columns ) );
			$query         = "INSERT INTO {$wpdb->yith_wcwl_items} ( {$query_columns} ) VALUES ( {$query_values} ) ";

			$res = $wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( $res ) {
				$item->set_id( $wpdb->insert_id );
				$item->apply_changes();
				$this->clear_cache( $item );

				/**
				 * DO_ACTION: yith_wcwl_new_wishlist_item
				 *
				 * Allows to fire some action when a new item is added to the wishlist.
				 *
				 * @param int                     $item_id     Wishlist item ID
				 * @param YITH_WCWL_Wishlist_Item $item        Wishlist item object
				 * @param int                     $wishlist_id Wishlist ID
				 */
				do_action( 'yith_wcwl_new_wishlist_item', $item->get_id(), $item, $item->get_wishlist_id() );
			}
		}

		/**
		 * Read/populate data properties specific to this order item.
		 *
		 * @param WC_Order_Item_Product $item Product order item object.
		 *
		 * @throws Exception When wishlist item is not found.
		 * @since 3.0.0
		 */
		public function read( &$item ) {
			global $wpdb;

			$item->set_defaults();

			// Get from cache if available.
			$data = wp_cache_get( 'item-' . $item->get_id(), 'wishlist-items' );

			if ( false === $data ) {
				$data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_items} WHERE ID = %d LIMIT 1;", $item->get_id() ) );
				wp_cache_set( 'item-' . $item->get_id(), $data, 'wishlist-items' );
			}

			if ( ! $data ) {
				throw new Exception( esc_html__( 'Invalid wishlist item.', 'yith-woocommerce-wishlist' ) );
			}

			$item->set_props(
				array(
					'wishlist_id'       => $data->wishlist_id,
					'product_id'        => $data->prod_id,
					'user_id'           => $data->user_id,
					'quantity'          => $data->quantity,
					'date_added'        => $data->dateadded,
					'position'          => $data->position,
					'original_price'    => $data->original_price,
					'original_currency' => $data->original_currency,
					'on_sale'           => $data->on_sale,
				)
			);
			$item->set_object_read( true );
		}

		/**
		 * Update a wishlist item in the database.
		 *
		 * @param YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 * @since 3.0.0
		 */
		public function update( &$item ) {
			if ( ! $item->get_id() ) {
				return;
			}

			$changes = $item->get_changes();

			if ( array_intersect( array( 'quantity', 'wishlist_id', 'product_id', 'user_id', 'position', 'on_sale', 'date_added' ), array_keys( $changes ) ) ) {
				$columns = array(
					'quantity'    => '%d',
					'wishlist_id' => '%d',
					'prod_id'     => '%d',
					'position'    => '%d',
					'on_sale'     => '%d',
					'dateadded'   => 'FROM_UNIXTIME( %d )',
					'user_id'     => $item->get_user_id() ? '%d' : 'NULL',
				);
				$values  = array(
					$item->get_quantity(),
					$item->get_wishlist_id(),
					$item->get_original_product_id(),
					$item->get_position(),
					$item->is_on_sale(),
					$item->get_date_added( 'edit' ) ? $item->get_date_added( 'edit' )->getTimestamp() : time(),
				);

				$user_id = $item->get_user_id();

				if ( $user_id ) {
					$values[] = $user_id;
				}

				$this->update_raw( $columns, $values, array( 'ID' => '%d' ), array( $item->get_id() ) );
			}

			$item->apply_changes();
			$this->clear_cache( $item );

			/**
			 * DO_ACTION: yith_wcwl_update_wishlist_item
			 *
			 * Allows to fire some action when an item is updated in the wishlist.
			 *
			 * @param int                     $item_id     Wishlist item ID
			 * @param YITH_WCWL_Wishlist_Item $item        Wishlist item object
			 * @param int                     $wishlist_id Wishlist ID
			 */
			do_action( 'yith_wcwl_update_wishlist_item', $item->get_id(), $item, $item->get_wishlist_id() );
		}

		/**
		 * Remove a wishlist item from the database.
		 *
		 * @param \YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 * @since 3.0.0
		 */
		public function delete( &$item ) {
			global $wpdb;

			$id = $item->get_id();

			if ( ! $id ) {
				return;
			}

			/**
			 * DO_ACTION: yith_wcwl_before_delete_wishlist_item
			 *
			 * Allows to fire some action before an item is deleted from the wishlist.
			 *
			 * @param int $item_id Wishlist item ID
			 */
			do_action( 'yith_wcwl_before_delete_wishlist_item', $item->get_id() );

			$wpdb->delete( $wpdb->yith_wcwl_items, array( 'ID' => $item->get_id() ) );

			/**
			 * DO_ACTION: yith_wcwl_delete_wishlist_item
			 *
			 * Allows to fire some action when an item is deleted from the wishlist.
			 *
			 * @param int $item_id Wishlist item ID
			 */
			do_action( 'yith_wcwl_delete_wishlist_item', $item->get_id() );

			$item->set_id( 0 );
			$this->clear_cache( $item );
		}

		/**
		 * Retrieves wishlist items that match a set of conditions
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 *                    [<br/>
		 *                    'user_id'             // Owner of the wishlist; default to current user logged in (if any), or false for cookie wishlist<br/>
		 *                    'product_id'          // Product to search in the wishlist<br/>
		 *                    'wishlist_id'         // wishlist_id for a specific wishlist, false for default, or all for any wishlist<br/>
		 *                    'wishlist_token'      // wishlist token, or false as default<br/>
		 *                    'wishlist_visibility' // all, visible, public, shared, private<br/>
		 *                    'is_default' =>       // whether searched wishlist should be default one <br/>
		 *                    'id' => false,        // only for table select<br/>
		 *                    'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *                    'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 *                    ].
		 *
		 * @return YITH_WCWL_Wishlist_Item[]
		 */
		public function query( $args = array() ) {
			global $wpdb;

			$default = array(
				'user_id'             => ( is_user_logged_in() ) ? get_current_user_id() : false,
				'session_id'          => ( ! is_user_logged_in() ) ? YITH_WCWL_Session()->maybe_get_session_id() : false,
				'product_id'          => false,
				'wishlist_id'         => false, // wishlist_id for a specific wishlist, false for default, or all for any wishlist.
				'wishlist_token'      => false,
				/**
				 * APPLY_FILTERS: yith_wcwl_wishlist_visibility_string_value
				 *
				 * Filter the wishlist visibility value.
				 *
				 * @param string $wishlist_visibility Wishlist visibility. Possible values are: all | visible | public | shared | private
				 *
				 * @return string
				 */
				'wishlist_visibility' => apply_filters( 'yith_wcwl_wishlist_visibility_string_value', 'all' ), // all | visible | public | shared | private.
				'is_default'          => false,
				'on_sale'             => false,
				'id'                  => false, // only for table select..
				'limit'               => false,
				'offset'              => 0,
				'orderby'             => '',
				'order'               => 'DESC',
			);

			// if there is no current wishlist, and user was asking for current one, short-circuit query, as pointless.
			if ( ! is_user_logged_in() && ! YITH_WCWL_Session()->has_session() && ! isset( $args['user_id'] ) && ! isset( $args['session_id'] ) ) {
				return array();
			}

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT SQL_CALC_FOUND_ROWS i.*
                    FROM `{$wpdb->yith_wcwl_items}` AS i
                    LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON l.`ID` = i.`wishlist_id`
                    INNER JOIN {$wpdb->posts} AS p ON p.ID = i.prod_id 
                    WHERE 1 AND p.post_type IN ( %s, %s ) AND p.post_status = %s";

			// remove hidden products from result.
			$hidden_products = yith_wcwl_get_hidden_products();

			/**
			 * APPLY_FILTERS: yith_wcwl_remove_hidden_products_via_query
			 *
			 * Filter whether to remove hidden products via query.
			 *
			 * @param bool $bool Remove hidden products or not?
			 *
			 * @return bool
			 */
			if ( ! empty( $hidden_products ) && apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
				$sql .= ' AND p.ID NOT IN ( ' . implode( ', ', array_filter( $hidden_products, 'esc_sql' ) ) . ' )';
			}

			$sql_args = array(
				'product',
				'product_variation',
				'publish',
			);

			if ( ! empty( $user_id ) ) {
				$sql        .= ' AND i.`user_id` = %d';
				$sql_args[] = $user_id;
			}

			if ( ! empty( $session_id ) ) {
				$sql        .= ' AND l.`session_id` = %s AND l.`expiration` > NOW()';
				$sql_args[] = $session_id;
			}

			if ( ! empty( $product_id ) ) {
				$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

				$sql        .= ' AND i.`prod_id` = %d';
				$sql_args[] = $product_id;
			}

			if ( ! empty( $wishlist_id ) && 'all' !== $wishlist_id ) {
				$sql        .= ' AND i.`wishlist_id` = %d';
				$sql_args[] = $wishlist_id;
			} elseif ( ( empty( $wishlist_id ) ) && empty( $wishlist_token ) && empty( $is_default ) ) {
				$sql .= ' AND i.`wishlist_id` IS NULL';
			}

			if ( ! empty( $wishlist_token ) ) {
				$sql        .= ' AND l.`wishlist_token` = %s';
				$sql_args[] = $wishlist_token;
			}

			if ( ! empty( $wishlist_visibility ) && 'all' !== $wishlist_visibility ) {
				switch ( $wishlist_visibility ) {
					case 'visible':
						$sql        .= ' AND ( l.`wishlist_privacy` = %d OR l.`wishlist_privacy` = %d )';
						$sql_args[] = 0;
						$sql_args[] = 1;
						break;
					case 'shared':
						$sql        .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 1;
						break;
					case 'private':
						$sql        .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 2;
						break;
					case 'public':
					default:
						$sql        .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 0;
						break;
				}
			}

			if ( ! empty( $is_default ) ) {
				YITH_WCWL_Wishlist_Factory::generate_default_wishlist();

				$sql        .= ' AND l.`is_default` = %d';
				$sql_args[] = $is_default;
			}

			if ( isset( $on_sale ) && false !== $on_sale ) {
				$sql        .= ' AND i.`on_sale` = %d';
				$sql_args[] = $on_sale;
			}

			if ( ! empty( $id ) ) {
				$sql        .= ' AND `i.ID` = %d';
				$sql_args[] = $id;
			}

			$sql .= ' GROUP BY i.prod_id, l.ID';

			if ( ! empty( $orderby ) ) {
				$order = ! empty( $order ) ? $order : 'DESC';
				$sql   .= ' ORDER BY i.' . esc_sql( $orderby ) . ' ' . esc_sql( $order ) . ', i.position ASC';
			} else {
				$sql .= ' ORDER BY i.position ASC, i.ID DESC';
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql        .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			$items = $wpdb->get_results( $wpdb->prepare( $sql, $sql_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			/**
			 * This filter was added to allow developer remove hidden products using a foreach loop, instead of the query
			 * It is required when the store contains a huge number of hidden products, and the resulting query would fail
			 * to be submitted to DBMS because of its size
			 *
			 * This code requires reasonable amount of products in the wishlist
			 * A great number of products retrieved from the main query could easily degrade performance of the overall system
			 *
			 * @since 3.0.7
			 */
			if ( ! empty( $hidden_products ) && ! empty( $items ) && ! apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
				foreach ( $items as $item_id => $item ) {
					if ( ! in_array( $item->prod_id, $hidden_products, true ) ) {
						continue;
					}

					unset( $items[ $item_id ] );
				}
			}

			if ( ! empty( $items ) ) {
				$items = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist_item' ), $items );
			} else {
				$items = array();
			}

			/**
			 * APPLY_FILTERS: yith_wcwl_get_products
			 *
			 * Filter the products retrieved from the wishlist.
			 *
			 * @param array $items Array of products
			 * @param array $args  Array of arguments
			 *
			 * @return array
			 */
			return apply_filters( 'yith_wcwl_get_products', $items, $args );
		}

		/**
		 * Counts items that matches
		 *
		 * @param array $args Same parameters allowed for {@see query} method.
		 * @return int Count of items
		 */
		public function count( $args = array() ) {
			return count( $this->query( $args ) );
		}

		/**
		 * Query items table to retrieve distinct products added to wishlist, with count of occurrences
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 *                    [<br/>
		 *                    'product_id'          // Product to search in the wishlist<br/>
		 *                    'search' => '',       // search string; will be matched against product name<br/>
		 *                    'interval' => '',     // Interval of dates; this should be an associative array, that may contain start_date or end_date<br/>
		 *                    'orderby' => 'ID',    // order param; a valid column in the result set<br/>
		 *                    'order' => 'desc',    // order param; asc or desc<br/>
		 *                    'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *                    'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 *                    ].
		 * @return mixed Result set
		 */
		public function query_products( $args ) {
			global $wpdb;

			$default = array(
				'product_id' => '',
				'search'     => '',
				'interval'   => array(),
				'limit'      => false,
				'offset'     => 0,
				'orderby'    => 'ID',
				'order'      => 'DESC',
			);

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT
		            DISTINCT i.prod_id AS id,
		            p.post_title AS post_title,
		            i2.wishlist_count AS wishlist_count
		            FROM {$wpdb->yith_wcwl_items} AS i
		            INNER JOIN {$wpdb->posts} AS p ON p.ID = i.prod_id
		            LEFT JOIN ( 
		                SELECT 
		                COUNT( DISTINCT ID ) AS wishlist_count, 
                        prod_id 
		                FROM {$wpdb->yith_wcwl_items} 
		                GROUP BY prod_id 
	                ) AS i2 ON p.ID = i2.prod_id
		            WHERE 1=1 AND p.post_status = %s";

			$sql_args = array( 'publish' );

			if ( ! empty( $product_id ) ) {
				$sql        .= ' AND i.prod_id = %d';
				$sql_args[] = $product_id;
			}

			if ( ! empty( $search ) ) {
				$sql        .= ' AND p.post_title LIKE %s';
				$sql_args[] = '%' . $search . '%';
			}

			if ( ! empty( $args['interval'] ) && is_array( $args['interval'] ) && ( isset( $args['interval']['start_date'] ) || isset( $args['interval']['end_date'] ) ) ) {
				if ( ! empty( $args['interval']['start_date'] ) ) {
					$sql        .= ' AND i.dateadded >= %s';
					$sql_args[] = $args['interval']['start_date'];
				}

				if ( ! empty( $args['interval']['end_date'] ) ) {
					$sql        .= ' AND i.dateadded <= %s';
					$sql_args[] = $args['interval']['end_date'];
				}
			}

			if ( ! empty( $orderby ) ) {
				$order = ! empty( $order ) ? $order : 'DESC';
				$sql   .= ' ORDER BY ' . esc_sql( $orderby ) . ' ' . esc_sql( $order );
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql        .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			$items = $wpdb->get_results( $wpdb->prepare( $sql, $sql_args ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			return $items;
		}

		/**
		 * Counts total number of distinct products added to wishlist
		 *
		 * @param array $args Same parameters allowed for {@see query_products} method.
		 * @return int Count of items
		 */
		public function count_products( $args ) {
			return count( $this->query_products( $args ) );
		}

		/**
		 * Counts how many distinct users added a product in wishlist
		 *
		 * @param int        $product_id Product id.
		 * @param string|int $user       User to use in query; it could be a user id, a session id or 'current' (for current user/session).
		 * @return int Count of times product was added to wishlist
		 */
		public function count_times_added( $product_id, $user = false ) {
			global $wpdb;

			$query_args     = array();
			$user_condition = '';

			if ( $user ) {
				if ( 'current' === $user ) {
					if ( is_user_logged_in() ) {
						$user_condition = ' AND l.`user_id` = %d';
						$query_args[]   = get_current_user_id();
					} elseif ( YITH_WCWL_Session()->has_session() ) {
						$user_condition = ' AND l.`session_id` = %s';
						$query_args[]   = YITH_WCWL_Session()->get_session_id();
					} else {
						return 0;
					}
				} elseif ( is_int( $user ) ) {
					$user_condition = ' AND l.`user_id` = %d';
					$query_args[]   = $user;
				} elseif ( is_string( $user ) ) {
					$user_condition = ' AND l.`session_id` = %s';
					$query_args[]   = $user;
				}
			}

			$query = "SELECT 
       				      COUNT( DISTINCT( v.`u_id` ) ) 
					  FROM ( 
					      SELECT 
					          ( CASE WHEN l.`user_id` IS NULL THEN l.`session_id` ELSE l.`user_id` END) AS u_id, 
					          l.`ID` as wishlist_id 
					      FROM {$wpdb->yith_wcwl_wishlists} AS l 
					      WHERE ( l.`expiration` > NOW() OR l.`expiration` IS NULL ) {$user_condition}
				      ) as v 
				      LEFT JOIN {$wpdb->yith_wcwl_items} AS i USING( wishlist_id ) 
					  WHERE i.`prod_id` = %d";

			$query_args[] = $product_id;

			$res = $wpdb->get_var( $wpdb->prepare( $query, $query_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			return (int) $res;
		}

		/**
		 * Raw update method; useful when it is needed to update a bunch of items
		 *
		 * @param array $columns           Array of columns to update, in the following format: 'column_id' => 'column_type'.
		 * @param array $column_values     Array of values to apply to the query; must have same number of elements of columns, and they must respect defined tpe.
		 * @param array $conditions        Array of where conditions, in the following format: 'column_id' => 'columns_type'.
		 * @param array $conditions_values Array of values to apply to where condition; must have same number of elements of columns, and they must respect defined type.
		 * @param bool  $clear_caches      Whether system should clear caches (this is optional since other methods may want to run more optimized clear).
		 *
		 * @return void
		 */
		public function update_raw( $columns, $column_values, $conditions = array(), $conditions_values = array(), $clear_caches = false ) {
			global $wpdb;

			// calculate where statement.
			$query_where = '';

			if ( ! empty( $conditions ) ) {
				$query_where = array();

				foreach ( $conditions as $column => $value ) {
					$query_where[] = $column . '=' . $value;
				}

				$query_where = ' WHERE ' . implode( ' AND ', $query_where );
			}

			// retrieves wishlists that will be affected by the changes.
			if ( $clear_caches ) {
				$query = "SELECT ID FROM {$wpdb->yith_wcwl_items} {$query_where}";
				$query = $conditions ? $wpdb->prepare( $query, $conditions_values ) : $query; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$ids   = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// calculate set statement.
			$query_columns = array();

			foreach ( $columns as $column => $value ) {
				$query_columns[] = $column . '=' . $value;
			}

			$query_columns = implode( ', ', $query_columns );

			// build query, and execute it.
			$query  = "UPDATE {$wpdb->yith_wcwl_items} SET {$query_columns} {$query_where}";
			$values = $conditions ? array_merge( $column_values, $conditions_values ) : $column_values;

			$wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			// clear cache for updated items.
			if ( $clear_caches && $ids ) {
				foreach ( $ids as $id ) {
					$this->clear_cache( $id );
				}
			}
		}

		/**
		 * Clear meta cache.
		 *
		 * @param YITH_WCWL_Wishlist_Item|int $item Wishlist item object, or id of the item.
		 */
		public function clear_cache( &$item ) {
			if ( ! $item instanceof YITH_WCWL_Wishlist_Item ) {
				$item = YITH_WCWL_Wishlist_Factory::get_wishlist_item( $item );
			}

			wp_cache_delete( 'item-' . $item->get_id(), 'wishlist-items' );
			wp_cache_delete( 'wishlist-items-' . $item->get_wishlist_id(), 'wishlists' );
			wp_cache_delete( 'wishlist-items-' . $item->get_origin_wishlist_id(), 'wishlists' );
		}

		/* === MISC === */

		/**
		 * Here we collected all methods related to db implementation of the items
		 * They can be used without creating an instance of the Data Store, and are
		 * listed here just for
		 */

		/**
		 * Alter join section of the query, for ordering purpose
		 *
		 * @param string $join Join statement of the query.
		 * @return string
		 * @since 2.0.0
		 */
		public static function filter_join_for_wishlist_count( $join ) {
			global $wpdb;
			$join .= " LEFT JOIN ( SELECT COUNT(*) AS wishlist_count, prod_id FROM {$wpdb->yith_wcwl_items} GROUP BY prod_id ) AS i ON ID = i.prod_id";
			return $join;
		}

		/**
		 * Alter orderby section of the query, for ordering purpose
		 *
		 * @param string $orderby Orderby statement of the query.
		 * @return string
		 * @since 2.0.0
		 */
		public static function filter_orderby_for_wishlist_count( $orderby ) {
			$order   = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'ASC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$orderby = 'i.wishlist_count ' . $order;

			return $orderby;
		}

		protected function get_db_info() {
			global $wpdb;

			$db_info          = parent::get_db_info();
			$db_info['table'] = $wpdb->yith_wcwl_itemmeta;

			return $db_info;
		}
	}
}

// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
abstract-wc-order-data-store-cpt.php000064400000060735151542600450013442 0ustar00<?php
/**
 * Abstract_WC_Order_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract Order Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Abstract_Order_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Internal meta type used to store order data.
	 *
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_order_currency',
		'_cart_discount',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_order_version',
		'_prices_include_tax',
		'_payment_tokens',
	);

	/**
	 * Custom setters for props. Add key here if it has corresponding set_ and get_ method present.
	 *
	 * @var string[]
	 */
	protected $internal_data_store_key_getters = array();

	/**
	 * Return internal key getters name.
	 *
	 * @return string[]
	 */
	public function get_internal_data_store_key_getters() {
		return $this->internal_data_store_key_getters;
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Method to create a new order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function create( &$order ) {
		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
		$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
		if ( ! $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_order_data',
				array(
					'post_date'     => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
					'post_type'     => $order->get_type( 'edit' ),
					'post_status'   => $this->get_post_status( $order ),
					'ping_status'   => 'closed',
					'post_author'   => 1,
					'post_title'    => $this->get_post_title(),
					'post_password' => $this->get_order_key( $order ),
					'post_parent'   => $order->get_parent_id( 'edit' ),
					'post_excerpt'  => $this->get_post_excerpt( $order ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$order->set_id( $id );
			$this->update_post_meta( $order );
			$order->save_meta_data();
			$order->apply_changes();
			$this->clear_caches( $order );
		}
	}

	/**
	 * Check if an order exists by id.
	 *
	 * @since 8.0.0
	 *
	 * @param int $order_id The order id to check.
	 * @return bool True if an order exists with the given name.
	 */
	public function order_exists( $order_id ) : bool {
		if ( ! $order_id ) {
			return false;
		}

		$post_object = get_post( $order_id );
		return ! is_null( $post_object ) && in_array( $post_object->post_type, wc_get_order_types(), true );
	}

	/**
	 * Method to read an order from the database.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @throws Exception If passed order is invalid.
	 */
	public function read( &$order ) {
		$order->set_defaults();
		$post_object = get_post( $order->get_id() );
		if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
			throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
		}

		$this->set_order_props(
			$order,
			array(
				'parent_id'     => $post_object->post_parent,
				'date_created'  => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'        => $post_object->post_status,
			)
		);

		$this->read_order_data( $order, $post_object );
		$order->read_meta_data();
		$order->set_object_read( true );

		/**
		 * In older versions, discounts may have been stored differently.
		 * Update them now so if the object is saved, the correct values are
		 * stored.
		 */
		if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) {
			$order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) );
		}
	}

	/**
	 * Set the properties of an object and log the first error found while doing so.
	 *
	 * @param $order WC_Order $order Order object.
	 * @param array          $props The properties to set.
	 */
	private function set_order_props( &$order, array $props ) {
		$errors = $order->set_props( $props );

		if ( ! $errors instanceof WP_Error ) {
			return;
		}

		$order_id = $order->get_id();
		$logger   = WC()->call_function( 'wc_get_logger' );

		foreach ( $errors->get_error_codes() as $error_code ) {
			$property_name = $errors->get_error_data( $error_code )['property_name'] ?? '';
			$error_message = $errors->get_error_message( $error_code );
			$logger->warning(
				sprintf(
				/* translators: %1$s = order ID, %2$s = order id, %3$s = error message. */
					__( 'Error when setting property \'%1$s\' for order %2$d: %3$s', 'woocommerce' ),
					$property_name,
					$order_id,
					$error_message
				),
				array(
					'error_code'    => $error_code,
					'error_message' => $error_message,
					'order_id'      => $order_id,
					'property_name' => $property_name,
				)
			);
		}
	}

	/**
	 * Method to update an order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function update( &$order ) {
		$order->save_meta_data();
		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );

		if ( null === $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$changes = $order->get_changes();

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_date'         => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
				'post_status'       => $this->get_post_status( $order ),
				'post_parent'       => $order->get_parent_id(),
				'post_excerpt'      => $this->get_post_excerpt( $order ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) );
				clean_post_cache( $order->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) );
			}
			$order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
		}
		$this->update_post_meta( $order );
		$order->apply_changes();
		$this->clear_caches( $order );
	}

	/**
	 * Method to delete an order from the database.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $args Array of args to pass to the delete method.
	 *
	 * @return void
	 */
	public function delete( &$order, $args = array() ) {
		$id   = $order->get_id();
		$args = wp_parse_args(
			$args,
			array(
				'force_delete'     => false,
				'suppress_filters' => false,
			)
		);

		if ( ! $id ) {
			return;
		}

		$do_filters = ! $args['suppress_filters'];

		if ( $args['force_delete'] ) {
			if ( $do_filters ) {
				/**
				 * Fires immediately before an order is deleted from the database.
				 *
				 * @since 8.0.0
				 *
				 * @param int      $order_id ID of the order about to be deleted.
				 * @param WC_Order $order    Instance of the order that is about to be deleted.
				 */
				do_action( 'woocommerce_before_delete_order', $id, $order );
			}

			wp_delete_post( $id );
			$order->set_id( 0 );

			if ( $do_filters ) {
				/**
				 * Fires immediately after an order is deleted.
				 *
				 * @since
				 *
				 * @param int $order_id ID of the order that has been deleted.
				 */
				do_action( 'woocommerce_delete_order', $id );
			}
		} else {
			if ( $do_filters ) {
				/**
				 * Fires immediately before an order is trashed.
				 *
				 * @since 8.0.0
				 *
				 * @param int      $order_id ID of the order about to be trashed.
				 * @param WC_Order $order    Instance of the order that is about to be trashed.
				 */
				do_action( 'woocommerce_before_trash_order', $id, $order );
			}

			wp_trash_post( $id );
			$order->set_status( 'trash' );

			if ( $do_filters ) {
				/**
				 * Fires immediately after an order is trashed.
				 *
				 * @since
				 *
				 * @param int      $order_id ID of the order that has been trashed.
				 */
				do_action( 'woocommerce_trash_order', $id );
			}
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the status to save to the post object.
	 *
	 * Plugins extending the order classes can override this to change the stored status/add prefixes etc.
	 *
	 * @since 3.6.0
	 * @param  WC_order $order Order object.
	 * @return string
	 */
	protected function get_post_status( $order ) {
		$order_status = $order->get_status( 'edit' );

		if ( ! $order_status ) {
			$order_status = apply_filters( 'woocommerce_default_order_status', 'pending' );
		}

		$post_status    = $order_status;
		$valid_statuses = get_post_stati();

		// Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed.
		// @todo In the future this should only happen based on `wc_is_order_status`, but in order to
		// preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong
		// Notice will be needed here, followed by future removal.
		if ( ! in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) {
			$post_status = 'wc-' . $post_status;
		}

		return $post_status;
	}

	/**
	 * Excerpt for post.
	 *
	 * @param  WC_order $order Order object.
	 * @return string
	 */
	protected function get_post_excerpt( $order ) {
		return '';
	}

	/**
	 * Get a title for the new post type.
	 *
	 * @return string
	 */
	protected function get_post_title() {
		// @codingStandardsIgnoreStart
		/* translators: %s: Order date */
		return sprintf( __( 'Order &ndash; %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) );
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Get order key.
	 *
	 * @since 4.3.0
	 * @param WC_order $order Order object.
	 * @return string
	 */
	protected function get_order_key( $order ) {
		return wc_generate_order_key();
	}

	/**
	 * Read order data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order $order Order object.
	 * @param object   $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$order, $post_object ) {
		$id = $order->get_id();

		$this->set_order_props(
			$order,
			array(
				'currency'           => get_post_meta( $id, '_order_currency', true ),
				'discount_total'     => get_post_meta( $id, '_cart_discount', true ),
				'discount_tax'       => get_post_meta( $id, '_cart_discount_tax', true ),
				'shipping_total'     => get_post_meta( $id, '_order_shipping', true ),
				'shipping_tax'       => get_post_meta( $id, '_order_shipping_tax', true ),
				'cart_tax'           => get_post_meta( $id, '_order_tax', true ),
				'total'              => get_post_meta( $id, '_order_total', true ),
				'version'            => get_post_meta( $id, '_order_version', true ),
				'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === get_post_meta( $id, '_prices_include_tax', true ) : 'yes' === get_option( 'woocommerce_prices_include_tax' ),
			)
		);

		// Gets extra data associated with the order if needed.
		foreach ( $order->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $order, $function ) ) ) {
				$order->{$function}( get_post_meta( $order->get_id(), '_' . $key, true ) );
			}
		}
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$order ) {
		$updated_props     = array();
		$meta_key_to_props = array(
			'_order_currency'     => 'currency',
			'_cart_discount'      => 'discount_total',
			'_cart_discount_tax'  => 'discount_tax',
			'_order_shipping'     => 'shipping_total',
			'_order_shipping_tax' => 'shipping_tax',
			'_order_tax'          => 'cart_tax',
			'_order_total'        => 'total',
			'_order_version'      => 'version',
			'_prices_include_tax' => 'prices_include_tax',
		);

		$props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $order->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;

			if ( 'prices_include_tax' === $prop ) {
				$value = $value ? 'yes' : 'no';
			}

			$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

			if ( $updated ) {
				$updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
	}

	/**
	 * Clear any caches.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function clear_caches( &$order ) {
		clean_post_cache( $order->get_id() );
		wc_delete_shop_order_transients( $order );
		wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' );
	}

	/**
	 * Read order items of a specific type from the database for this order.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  string   $type Order item type.
	 * @return array
	 */
	public function read_items( $order, $type ) {
		global $wpdb;

		// Get from cache if available.
		$items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false;

		if ( false === $items ) {
			$items = $wpdb->get_results(
				$wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() )
			);
			foreach ( $items as $item ) {
				wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
			}
			if ( 0 < $order->get_id() ) {
				wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' );
			}
		}

		$items = wp_list_filter( $items, array( 'order_item_type' => $type ) );

		if ( ! empty( $items ) ) {
			$items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) );
		} else {
			$items = array();
		}

		return $items;
	}

	/**
	 * Return the order type of a given item which belongs to WC_Order.
	 *
	 * @since  3.2.0
	 * @param  WC_Order $order Order Object.
	 * @param  int      $order_item_id Order item id.
	 * @return string Order Item type
	 */
	public function get_order_item_type( $order, $order_item_id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) );
	}

	/**
	 * Prime following caches:
	 *  1. item-$order_item_id   For individual items.
	 *  2. order-items-$order-id For fetching items associated with an order.
	 *  3. order-item meta.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	protected function prime_order_item_caches_for_orders( $order_ids, $query_vars ) {
		global $wpdb;
		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			$line_items = array(
				'line_items',
				'shipping_lines',
				'fee_lines',
				'coupon_lines',
			);

			if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) {
				return;
			}
		}
		$cache_keys     = array_map(
			function ( $order_id ) {
				return 'order-items-' . $order_id;
			},
			$order_ids
		);
		$cache_values   = wc_cache_get_multiple( $cache_keys, 'orders' );
		$non_cached_ids = array();
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ 'order-items-' . $order_id ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}

		$non_cached_ids        = esc_sql( $non_cached_ids );
		$non_cached_ids_string = implode( ',', $non_cached_ids );
		$order_items           = $wpdb->get_results(
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;"
		);
		if ( empty( $order_items ) ) {
			return;
		}

		$order_items_for_all_orders = array_reduce(
			$order_items,
			function ( $order_items_collection, $order_item ) {
				if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) {
					$order_items_collection[ $order_item->order_id ] = array();
				}
				$order_items_collection[ $order_item->order_id ][] = $order_item;
				return $order_items_collection;
			}
		);
		foreach ( $order_items_for_all_orders as $order_id => $items ) {
			wp_cache_set( 'order-items-' . $order_id, $items, 'orders' );
		}
		foreach ( $order_items as $item ) {
			wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
		}
		$order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
		update_meta_cache( 'order_item', $order_item_ids );
	}

	/**
	 * Remove all line items (products, coupons, shipping, taxes) from the order.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $type Order item type. Default null.
	 */
	public function delete_items( $order, $type = null ) {
		global $wpdb;
		if ( ! empty( $type ) ) {
			$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order->get_id(), $type ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order->get_id(), $type ) );
		} else {
			$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order->get_id() ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order->get_id() ) );
		}
		$this->clear_caches( $order );
	}

	/**
	 * Get token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	public function get_payment_token_ids( $order ) {
		$token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) );
		return $token_ids;
	}

	/**
	 * Update token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $token_ids Payment token ids.
	 */
	public function update_payment_token_ids( $order, $token_ids ) {
		update_post_meta( $order->get_id(), '_payment_tokens', $token_ids );
	}

	/**
	 * Get the order's title.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return string Order title.
	 */
	public function get_title( WC_Order $order ) {
		return get_the_title( $order->get_id() );
	}

	/**
	 * Given an initialized order object, update the post/postmeta records.
	 *
	 * @param WC_Abstract_Order $order Order object.
	 *
	 * @return bool Whether the order was updated.
	 */
	public function update_order_from_object( $order ) {
		if ( ! $order->get_id() ) {
			return false;
		}
		$this->update_order_meta_from_object( $order );

		// Add hook to update post_modified date so that it's the same as order. Without this hook, WP will set the modified date to current date, and we will think that posts and orders are out of sync again.
		add_filter( 'wp_insert_post_data', array( $this, 'update_post_modified_data' ), 10, 2 );
		$post_data = array(
			'ID'                 => $order->get_id(),
			'post_date'          => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
			'post_date_gmt'      => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
			'post_status'        => $this->get_post_status( $order ),
			'post_parent'        => $order->get_parent_id(),
			'edit_date'          => true,
			'post_excerpt'       => method_exists( $order, 'get_customer_note' ) ? $order->get_customer_note() : '',
			'post_type'          => $order->get_type(),
			'order_modified'     => ! is_null( $order->get_date_modified() ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : '',
			'order_modified_gmt' => ! is_null( $order->get_date_modified() ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : '',
		);
		$updated   = wp_update_post( $post_data );
		remove_filter( 'wp_insert_post_data', array( $this, 'update_post_modified_data' ) );
		return $updated;
	}

	/**
	 * Change the modified date of the post to match the order's modified date if passed.
	 *
	 * @hooked wp_insert_post_data See function update_order_from_object.
	 *
	 * @param array $data An array of slashed, sanitized, and processed post data.
	 * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
	 *
	 * @return array Data with updated modified date.
	 */
	public function update_post_modified_data( $data, $postarr ) {
		if ( ! isset( $postarr['order_modified'] ) || ! isset( $postarr['order_modified_gmt'] ) ) {
			return $data;
		}

		$data['post_modified']     = $postarr['order_modified'];
		$data['post_modified_gmt'] = $postarr['order_modified_gmt'];
		return $data;
	}

	/**
	 * Helper method to update order metadata from initialized order object.
	 *
	 * @param WC_Abstract_Order $order Order object.
	 */
	protected function update_order_meta_from_object( $order ) {
		if ( is_null( $order->get_meta() ) ) {
			return;
		}

		$existing_meta_data = get_post_meta( $order->get_id() );

		foreach ( $order->get_meta_data() as $meta_data ) {
			if ( isset( $existing_meta_data[ $meta_data->key ] ) ) {
				// We don't know if the meta is single or array, so we assume it to be an array.
				$meta_value = is_array( $meta_data->value ) ? $meta_data->value : array( $meta_data->value );

				if ( $existing_meta_data[ $meta_data->key ] === $meta_value ) {
					unset( $existing_meta_data[ $meta_data->key ] );
					continue;
				}

				if ( is_array( $existing_meta_data[ $meta_data->key ] ) ) {
					$value_index = array_search( $meta_data->value, $existing_meta_data[ $meta_data->key ], true );
					if ( false !== $value_index ) {
						unset( $existing_meta_data[ $meta_data->key ][ $value_index ] );
						if ( 0 === count( $existing_meta_data[ $meta_data->key ] ) ) {
							unset( $existing_meta_data[ $meta_data->key ] );
						}
						continue;
					}
				}
			}
			add_post_meta( $order->get_id(), $meta_data->key, $meta_data->value, false );
		}

		// Find remaining meta that was deleted from the order but still present in the associated post.
		// Post meta corresponding to order props is excluded (as it shouldn't be deleted).
		$keys_to_delete = array_diff(
			array_keys( $existing_meta_data ),
			$this->internal_meta_keys,
			array_keys( $this->get_internal_data_store_key_getters() )
		);

		foreach ( $keys_to_delete as $meta_key ) {
			if ( isset( $existing_meta_data[ $meta_key ] ) ) {
				foreach ( $existing_meta_data[ $meta_key ] as $meta_value ) {
					delete_post_meta( $order->get_id(), $meta_key, maybe_unserialize( $meta_value ) );
				}
			}
		}

		$this->update_post_meta( $order );
	}
}
abstract-wc-order-item-type-data-store.php000064400000010657151542600450014567 0ustar00<?php
/**
 * Class Abstract_WC_Order_Item_Type_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Data Store
 *
 * @version  3.0.0
 */
abstract class Abstract_WC_Order_Item_Type_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface {

	/**
	 * Meta type. This should match up with
	 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
	 * WP defines 'post', 'user', 'comment', and 'term'.
	 *
	 * @var string
	 */
	protected $meta_type = 'order_item';

	/**
	 * This only needs set if you are using a custom metadata type (for example payment tokens.
	 * This should be the name of the field your table uses for associating meta with objects.
	 * For example, in payment_tokenmeta, this would be payment_token_id.
	 *
	 * @var string
	 */
	protected $object_id_field_for_meta = 'order_item_id';

	/**
	 * Create a new order item in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function create( &$item ) {
		global $wpdb;

		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_order_items',
			array(
				'order_item_name' => $item->get_name(),
				'order_item_type' => $item->get_type(),
				'order_id'        => $item->get_order_id(),
			)
		);
		$item->set_id( $wpdb->insert_id );
		$this->save_item_data( $item );
		$item->save_meta_data();
		$item->apply_changes();
		$this->clear_cache( $item );

		do_action( 'woocommerce_new_order_item', $item->get_id(), $item, $item->get_order_id() );
	}

	/**
	 * Update a order item in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function update( &$item ) {
		global $wpdb;

		$changes = $item->get_changes();

		if ( array_intersect( array( 'name', 'order_id' ), array_keys( $changes ) ) ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_order_items',
				array(
					'order_item_name' => $item->get_name(),
					'order_item_type' => $item->get_type(),
					'order_id'        => $item->get_order_id(),
				),
				array( 'order_item_id' => $item->get_id() )
			);
		}

		$this->save_item_data( $item );
		$item->save_meta_data();
		$item->apply_changes();
		$this->clear_cache( $item );

		do_action( 'woocommerce_update_order_item', $item->get_id(), $item, $item->get_order_id() );
	}

	/**
	 * Remove an order item from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 * @param array         $args Array of args to pass to the delete method.
	 */
	public function delete( &$item, $args = array() ) {
		if ( $item->get_id() ) {
			global $wpdb;
			do_action( 'woocommerce_before_delete_order_item', $item->get_id() );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_id' => $item->get_id() ) );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_order_itemmeta', array( 'order_item_id' => $item->get_id() ) );
			do_action( 'woocommerce_delete_order_item', $item->get_id() );
			$this->clear_cache( $item );
		}
	}

	/**
	 * Read a order item from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Order_Item $item Order item object.
	 *
	 * @throws Exception If invalid order item.
	 */
	public function read( &$item ) {
		global $wpdb;

		$item->set_defaults();

		// Get from cache if available.
		$data = wp_cache_get( 'item-' . $item->get_id(), 'order-items' );

		if ( false === $data ) {
			$data = $wpdb->get_row( $wpdb->prepare( "SELECT order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item->get_id() ) );
			wp_cache_set( 'item-' . $item->get_id(), $data, 'order-items' );
		}

		if ( ! $data ) {
			throw new Exception( __( 'Invalid order item.', 'woocommerce' ) );
		}

		$item->set_props(
			array(
				'order_id' => $data->order_id,
				'name'     => $data->order_item_name,
			)
		);
		$item->read_meta_data();
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $item->get_id() will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function save_item_data( &$item ) {}

	/**
	 * Clear meta cache.
	 *
	 * @param WC_Order_Item $item Order item object.
	 */
	public function clear_cache( &$item ) {
		wp_cache_delete( 'item-' . $item->get_id(), 'order-items' );
		wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' );
		wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' );
	}
}
class-wc-coupon-data-store-cpt.php000064400000060747151542600450013137 0ustar00<?php
/**
 * Class WC_Coupon_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Coupon Data Store: Custom Post Type.
 *
 * @version  3.0.0
 */
class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Internal meta type used to store coupon data.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * Data stored in meta keys, but not considered "meta" for a coupon.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'discount_type',
		'coupon_amount',
		'expiry_date',
		'date_expires',
		'usage_count',
		'individual_use',
		'product_ids',
		'exclude_product_ids',
		'usage_limit',
		'usage_limit_per_user',
		'limit_usage_to_x_items',
		'free_shipping',
		'product_categories',
		'exclude_product_categories',
		'exclude_sale_items',
		'minimum_amount',
		'maximum_amount',
		'customer_email',
		'_used_by',
		'_edit_lock',
		'_edit_last',
	);

	/**
	 * The updated coupon properties
	 *
	 * @since 4.1.0
	 * @var array
	 */
	protected $updated_props = array();

	/**
	 * Method to create a new coupon in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 */
	public function create( &$coupon ) {
		if ( ! $coupon->get_date_created( 'edit' ) ) {
			$coupon->set_date_created( time() );
		}

		$coupon_id = wp_insert_post(
			apply_filters(
				'woocommerce_new_coupon_data',
				array(
					'post_type'     => 'shop_coupon',
					'post_status'   => 'publish',
					'post_author'   => get_current_user_id(),
					'post_title'    => $coupon->get_code( 'edit' ),
					'post_content'  => '',
					'post_excerpt'  => $coupon->get_description( 'edit' ),
					'post_date'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
					'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
				)
			),
			true
		);

		if ( $coupon_id ) {
			$coupon->set_id( $coupon_id );
			$this->update_post_meta( $coupon );
			$coupon->save_meta_data();
			$coupon->apply_changes();
			delete_transient( 'rest_api_coupons_type_count' );
			do_action( 'woocommerce_new_coupon', $coupon_id, $coupon );
		}
	}

	/**
	 * Method to read a coupon.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 *
	 * @throws Exception If invalid coupon.
	 */
	public function read( &$coupon ) {
		$coupon->set_defaults();

		$post_object = get_post( $coupon->get_id() );

		if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) {
			throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
		}

		$coupon_id = $coupon->get_id();
		$coupon->set_props(
			array(
				'code'                        => $post_object->post_title,
				'description'                 => $post_object->post_excerpt,
				'status'                      => $post_object->post_status,
				'date_created'                => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'               => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'date_expires'                => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
				'discount_type'               => get_post_meta( $coupon_id, 'discount_type', true ),
				'amount'                      => get_post_meta( $coupon_id, 'coupon_amount', true ),
				'usage_count'                 => get_post_meta( $coupon_id, 'usage_count', true ),
				'individual_use'              => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
				'product_ids'                 => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
				'excluded_product_ids'        => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
				'usage_limit'                 => get_post_meta( $coupon_id, 'usage_limit', true ),
				'usage_limit_per_user'        => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
				'limit_usage_to_x_items'      => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
				'free_shipping'               => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
				'product_categories'          => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
				'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
				'exclude_sale_items'          => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
				'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
				'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
				'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
				'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
			)
		);
		$coupon->read_meta_data();
		$coupon->set_object_read( true );
		do_action( 'woocommerce_coupon_loaded', $coupon );
	}

	/**
	 * Updates a coupon in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 */
	public function update( &$coupon ) {
		$coupon->save_meta_data();
		$changes = $coupon->get_changes();

		if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_title'        => $coupon->get_code( 'edit' ),
				'post_excerpt'      => $coupon->get_description( 'edit' ),
				'post_date'         => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
				clean_post_cache( $coupon->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
			}
			$coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
		}
		$this->update_post_meta( $coupon );
		$coupon->apply_changes();
		delete_transient( 'rest_api_coupons_type_count' );
		do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon );
	}

	/**
	 * Deletes a coupon from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @param array     $args Array of args to pass to the delete method.
	 */
	public function delete( &$coupon, $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'force_delete' => false,
			)
		);

		$id = $coupon->get_id();

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			wp_delete_post( $id );

			wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );

			$coupon->set_id( 0 );
			do_action( 'woocommerce_delete_coupon', $id );
		} else {
			wp_trash_post( $id );
			do_action( 'woocommerce_trash_coupon', $id );
		}
	}

	/**
	 * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @since 3.0.0
	 */
	private function update_post_meta( &$coupon ) {
		$meta_key_to_props = array(
			'discount_type'              => 'discount_type',
			'coupon_amount'              => 'amount',
			'individual_use'             => 'individual_use',
			'product_ids'                => 'product_ids',
			'exclude_product_ids'        => 'excluded_product_ids',
			'usage_limit'                => 'usage_limit',
			'usage_limit_per_user'       => 'usage_limit_per_user',
			'limit_usage_to_x_items'     => 'limit_usage_to_x_items',
			'usage_count'                => 'usage_count',
			'date_expires'               => 'date_expires',
			'free_shipping'              => 'free_shipping',
			'product_categories'         => 'product_categories',
			'exclude_product_categories' => 'excluded_product_categories',
			'exclude_sale_items'         => 'exclude_sale_items',
			'minimum_amount'             => 'minimum_amount',
			'maximum_amount'             => 'maximum_amount',
			'customer_email'             => 'email_restrictions',
		);

		$props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props );
		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $coupon->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'individual_use':
				case 'free_shipping':
				case 'exclude_sale_items':
					$value = wc_bool_to_string( $value );
					break;
				case 'product_ids':
				case 'excluded_product_ids':
					$value = implode( ',', array_filter( array_map( 'intval', $value ) ) );
					break;
				case 'product_categories':
				case 'excluded_product_categories':
					$value = array_filter( array_map( 'intval', $value ) );
					break;
				case 'email_restrictions':
					$value = array_filter( array_map( 'sanitize_email', $value ) );
					break;
				case 'date_expires':
					$value = $value ? $value->getTimestamp() : null;
					break;
			}

			$updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value );

			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props );
	}

	/**
	 * Increase usage count for current coupon.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon           Coupon object.
	 * @param string    $used_by          Either user ID or billing email.
	 * @param WC_Order  $order (Optional) If passed, clears the hold record associated with order.

	 * @return int New usage count.
	 */
	public function increase_usage_count( &$coupon, $used_by = '', $order = null ) {
		$coupon_held_key_for_user = '';
		if ( $order instanceof WC_Order ) {
			$coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() );
		}

		$new_count = $this->update_usage_count_meta( $coupon, 'increase' );

		if ( $used_by ) {
			$this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user );
			$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
		}

		do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by );

		return $new_count;
	}

	/**
	 * Helper function to add a `_used_by` record to track coupons used by the user.
	 *
	 * @param WC_Coupon $coupon           Coupon object.
	 * @param string    $used_by          Either user ID or billing email.
	 * @param string    $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record.
	 */
	private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) {
		global $wpdb;
		if ( $coupon_held_key && '' !== $coupon_held_key ) {
			// Looks like we added a tentative record for this coupon getting used.
			// Lets change the tentative record to a permanent one.
			$result = $wpdb->query(
				$wpdb->prepare(
					"
					UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1",
					'_used_by',
					$used_by,
					$coupon_held_key
				)
			);
			if ( ! $result ) {
				// If no rows were updated, then insert a `_used_by` row manually to maintain consistency.
				add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
			}
		} else {
			add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
		}
	}

	/**
	 * Decrease usage count for current coupon.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $used_by Either user ID or billing email.
	 * @return int New usage count.
	 */
	public function decrease_usage_count( &$coupon, $used_by = '' ) {
		global $wpdb;
		$new_count = $this->update_usage_count_meta( $coupon, 'decrease' );
		if ( $used_by ) {
			/**
			 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
			 * all instances where the key and value match, and we only want to delete one.
			 */
			$meta_id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;",
					$used_by,
					$coupon->get_id()
				)
			);
			if ( $meta_id ) {
				delete_metadata_by_mid( 'post', $meta_id );
				$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
			}
		}

		do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by );

		return $new_count;
	}

	/**
	 * Increase or decrease the usage count for a coupon by 1.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $operation 'increase' or 'decrease'.
	 * @return int New usage count
	 */
	private function update_usage_count_meta( &$coupon, $operation = 'increase' ) {
		global $wpdb;
		$id       = $coupon->get_id();
		$operator = ( 'increase' === $operation ) ? '+' : '-';

		add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
		$wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;",
				$id
			)
		);

		// Get the latest value direct from the DB, instead of possibly the WP meta cache.
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
	}

	/**
	 * Returns tentative usage count for coupon.
	 *
	 * @param int $coupon_id Coupon ID.
	 *
	 * @return int Tentative usage count.
	 */
	public function get_tentative_usage_count( $coupon_id ) {
		global $wpdb;
		return $wpdb->get_var(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$this->get_tentative_usage_query( $coupon_id )
		);
	}

	/**
	 * Get the number of uses for a coupon by user ID.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param int       $user_id User ID.
	 * @return int
	 */
	public function get_usage_by_user_id( &$coupon, $user_id ) {
		global $wpdb;
		$usage_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;",
				$coupon->get_id(),
				$user_id
			)
		);
		$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) );
		return $tentative_usage_count + $usage_count;
	}

	/**
	 * Get the number of uses for a coupon by email address
	 *
	 * @since 3.6.4
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $email Email address.
	 * @return int
	 */
	public function get_usage_by_email( &$coupon, $email ) {
		global $wpdb;
		$usage_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
				$coupon->get_id(),
				$email
			)
		);
		$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) );
		return $tentative_usage_count + $usage_count;
	}

	/**
	 * Get tentative coupon usages for user.
	 *
	 * @param int   $coupon_id    Coupon ID.
	 * @param array $user_aliases Array of user aliases to check tentative usages for.
	 *
	 * @return string|null
	 */
	public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) {
		global $wpdb;
		return $wpdb->get_var(
			$this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases )
		); // WPCS: unprepared SQL ok.

	}

	/**
	 * Get held time for resources before cancelling the order. Use 60 minutes as sane default.
	 * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds.
	 *
	 * @return int
	 */
	private function get_tentative_held_time() {
		return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60;
	}

	/**
	 * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 *
	 * @return bool|int|string|null Returns meta key if coupon was held, null if returned early.
	 */
	public function check_and_hold_coupon( $coupon ) {
		global $wpdb;

		$usage_limit = $coupon->get_usage_limit();
		$held_time   = $this->get_tentative_held_time();

		if ( 0 >= $usage_limit || 0 >= $held_time ) {
			return null;
		}

		if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
			return null;
		}

		// Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`.
		// We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple.
		if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) {
			$coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter.
			$coupon->save();
		}

		$query_for_usages = $wpdb->prepare(
			"
			SELECT meta_value from $wpdb->postmeta
			WHERE {$wpdb->postmeta}.meta_key = 'usage_count'
			AND {$wpdb->postmeta}.post_id = %d
			LIMIT 1
			FOR UPDATE
			",
			$coupon->get_id()
		);

		$query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() );
		$db_timestamp               = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM ' . $wpdb->posts . ' LIMIT 1' );

		$coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );

		$insert_statement = $wpdb->prepare(
			"
			INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
			SELECT %d, %s, %s FROM $wpdb->posts
			WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
			LIMIT 1",
			$coupon->get_id(),
			$coupon_usage_key,
			'',
			$usage_limit
		); // WPCS: unprepared SQL ok.

		/**
		 * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock.
		 * We will try to insert 3 times before giving up to recover from deadlock.
		 */
		for ( $count = 0; $count < 3; $count++ ) {
			$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
			if ( false !== $result ) {
				// Clear meta cache.
				wp_cache_delete( WC_Coupon::generate_meta_cache_key( $coupon->get_id(), 'coupons' ), 'coupons' );
				break;
			}
		}

		return $result > 0 ? $coupon_usage_key : $result;
	}

	/**
	 * Generate query to calculate tentative usages for the coupon.
	 *
	 * @param int $coupon_id Coupon ID to get tentative usage query for.
	 *
	 * @return string Query for tentative usages.
	 */
	private function get_tentative_usage_query( $coupon_id ) {
		global $wpdb;
		return $wpdb->prepare(
			"
			SELECT COUNT(meta_id) FROM $wpdb->postmeta
			WHERE {$wpdb->postmeta}.meta_key like %s
			AND {$wpdb->postmeta}.meta_key > %s
			AND {$wpdb->postmeta}.post_id = %d
			FOR UPDATE
			",
			array(
				'_coupon_held_%',
				'_coupon_held_' . time(),
				$coupon_id,
			)
		);  // WPCS: unprepared SQL ok.
	}

	/**
	 * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon.
	 *
	 * @param WC_Coupon $coupon       Coupon object.
	 * @param array     $user_aliases Emails or Ids to check for user.
	 * @param string    $user_alias   Email/ID to use as `used_by` value.
	 *
	 * @return null|false|int
	 */
	public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) {
		global $wpdb;
		$limit_per_user = $coupon->get_usage_limit_per_user();
		$held_time      = $this->get_tentative_held_time();

		if ( 0 >= $limit_per_user || 0 >= $held_time ) {
			// This coupon do not have any restriction for usage per customer. No need to check further, lets bail.
			return null;
		}

		if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
			return null;
		}

		$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );

		$query_for_usages = $wpdb->prepare(
			"
				SELECT COUNT(*) FROM $wpdb->postmeta
				WHERE {$wpdb->postmeta}.meta_key = '_used_by'
				AND {$wpdb->postmeta}.meta_value IN ('$format')
				AND {$wpdb->postmeta}.post_id = %d
				FOR UPDATE
				",
			array_merge(
				$user_aliases,
				array( $coupon->get_id() )
			)
		); // WPCS: unprepared SQL ok.

		$query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases );
		$db_timestamp               = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM ' . $wpdb->posts . ' LIMIT 1' );

		$coupon_used_by_meta_key    = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
		$insert_statement           = $wpdb->prepare(
			"
			INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
			SELECT %d, %s, %s FROM $wpdb->posts
			WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
			LIMIT 1",
			$coupon->get_id(),
			$coupon_used_by_meta_key,
			$user_alias,
			$limit_per_user
		); // WPCS: unprepared SQL ok.

		// This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is
		// high concurrency, in which case DB will abort the query which has done less work to resolve deadlock.
		// We will try up to 3 times before giving up.
		for ( $count = 0; $count < 3; $count++ ) {
			$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
			if ( false !== $result ) {
				// Clear meta cache.
				wp_cache_delete( WC_Coupon::generate_meta_cache_key( $coupon->get_id(), 'coupons' ), 'coupons' );
				break;
			}
		}

		return $result > 0 ? $coupon_used_by_meta_key : $result;
	}

	/**
	 * Generate query to calculate tentative usages for the coupon by the user.
	 *
	 * @param int   $coupon_id    Coupon ID.
	 * @param array $user_aliases List of user aliases to check for usages.
	 *
	 * @return string Tentative usages query.
	 */
	private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) {
		global $wpdb;

		$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );

		// Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query.
		return $wpdb->prepare(
			"
				SELECT COUNT( meta_id ) FROM $wpdb->postmeta
				WHERE {$wpdb->postmeta}.meta_key like %s
				AND {$wpdb->postmeta}.meta_key > %s
				AND {$wpdb->postmeta}.post_id = %d
				AND {$wpdb->postmeta}.meta_value IN ('$format')
				FOR UPDATE
				",
			array_merge(
				array(
					'_maybe_used_by_%',
					'_maybe_used_by_' . time(),
					$coupon_id,
				),
				$user_aliases
			)
		); // WPCS: unprepared SQL ok.
	}

	/**
	 * Return a coupon code for a specific ID.
	 *
	 * @since 3.0.0
	 * @param int $id Coupon ID.
	 * @return string Coupon Code
	 */
	public function get_code_by_id( $id ) {
		global $wpdb;
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_title
				FROM $wpdb->posts
				WHERE ID = %d
				AND post_type = 'shop_coupon'
				AND post_status = 'publish'",
				$id
			)
		);
	}

	/**
	 * Return an array of IDs for for a specific coupon code.
	 * Can return multiple to check for existence.
	 *
	 * @since 3.0.0
	 * @param string $code Coupon code.
	 * @return array Array of IDs.
	 */
	public function get_ids_by_code( $code ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC",
				wc_sanitize_coupon_code( $code )
			)
		);
	}
}
class-wc-customer-data-store-session.php000064400000012336151542600450014361 0ustar00<?php
/**
 * Class WC_Customer_Data_Store_Session file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Data Store which stores the data in session.
 *
 * @version  3.0.0
 */
class WC_Customer_Data_Store_Session extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Keys which are also stored in a session (so we can make sure they get updated...)
	 *
	 * @var array
	 */
	protected $session_keys = array(
		'id',
		'date_modified',
		'billing_postcode',
		'billing_city',
		'billing_address_1',
		'billing_address',
		'billing_address_2',
		'billing_state',
		'billing_country',
		'shipping_postcode',
		'shipping_city',
		'shipping_address_1',
		'shipping_address',
		'shipping_address_2',
		'shipping_state',
		'shipping_country',
		'is_vat_exempt',
		'calculated_shipping',
		'billing_first_name',
		'billing_last_name',
		'billing_company',
		'billing_phone',
		'billing_email',
		'shipping_first_name',
		'shipping_last_name',
		'shipping_company',
		'shipping_phone',
	);

	/**
	 * Simply update the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function create( &$customer ) {
		$this->save_to_session( $customer );
	}

	/**
	 * Simply update the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function update( &$customer ) {
		$this->save_to_session( $customer );
	}

	/**
	 * Saves all customer data to the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function save_to_session( $customer ) {
		$data = array();
		foreach ( $this->session_keys as $session_key ) {
			$function_key = $session_key;
			if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
				$session_key = str_replace( 'billing_', '', $session_key );
			}
			$data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' );
		}
		WC()->session->set( 'customer', $data );
	}

	/**
	 * Read customer data from the session unless the user has logged in, in
	 * which case the stored ID will differ from the actual ID.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	public function read( &$customer ) {
		$data = (array) WC()->session->get( 'customer' );

		/**
		 * There is a valid session if $data is not empty, and the ID matches the logged in user ID.
		 *
		 * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded.
		 */
		if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) {
			foreach ( $this->session_keys as $session_key ) {
				if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) {
					continue;
				}
				$function_key = $session_key;
				if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
					$session_key = str_replace( 'billing_', '', $session_key );
				}
				if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) {
					$customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) );
				}
			}
		}
		$this->set_defaults( $customer );
		$customer->set_object_read( true );
	}

	/**
	 * Load default values if props are unset.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	protected function set_defaults( &$customer ) {
		try {
			$default = wc_get_customer_default_location();
			$has_shipping_address = $customer->has_shipping_address();

			if ( ! $customer->get_billing_country() ) {
				$customer->set_billing_country( $default['country'] );
			}

			if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) {
				$customer->set_shipping_country( $customer->get_billing_country() );
			}

			if ( ! $customer->get_billing_state() ) {
				$customer->set_billing_state( $default['state'] );
			}

			if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) {
				$customer->set_shipping_state( $customer->get_billing_state() );
			}

			if ( ! $customer->get_billing_email() && is_user_logged_in() ) {
				$current_user = wp_get_current_user();
				$customer->set_billing_email( $current_user->user_email );
			}
		} catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
		}
	}

	/**
	 * Deletes a customer from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @param array       $args Array of args to pass to the delete method.
	 */
	public function delete( &$customer, $args = array() ) {
		WC()->session->set( 'customer', null );
	}

	/**
	 * Gets the customers last order.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return WC_Order|false
	 */
	public function get_last_order( &$customer ) {
		return false;
	}

	/**
	 * Return the number of orders this customer has.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return integer
	 */
	public function get_order_count( &$customer ) {
		return 0;
	}

	/**
	 * Return how much money this customer has spent.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return float
	 */
	public function get_total_spent( &$customer ) {
		return 0;
	}
}
class-wc-customer-data-store.php000064400000042167151542600450012705 0ustar00<?php
/**
 * Class WC_Customer_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Data Store.
 *
 * @version  3.0.0
 */
class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta".
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'locale',
		'billing_postcode',
		'billing_city',
		'billing_address_1',
		'billing_address_2',
		'billing_state',
		'billing_country',
		'shipping_postcode',
		'shipping_city',
		'shipping_address_1',
		'shipping_address_2',
		'shipping_state',
		'shipping_country',
		'paying_customer',
		'last_update',
		'first_name',
		'last_name',
		'display_name',
		'show_admin_bar_front',
		'use_ssl',
		'admin_color',
		'rich_editing',
		'comment_shortcuts',
		'dismissed_wp_pointers',
		'show_welcome_panel',
		'session_tokens',
		'nickname',
		'description',
		'billing_first_name',
		'billing_last_name',
		'billing_company',
		'billing_phone',
		'billing_email',
		'shipping_first_name',
		'shipping_last_name',
		'shipping_company',
		'shipping_phone',
		'wptests_capabilities',
		'wptests_user_level',
		'syntax_highlighting',
		'_order_count',
		'_money_spent',
		'_last_order',
		'_woocommerce_tracks_anon_id',
	);

	/**
	 * Internal meta type used to store user data.
	 *
	 * @var string
	 */
	protected $meta_type = 'user';

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object.
	 * @return bool
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		global $wpdb;

		$table_prefix = $wpdb->prefix ? $wpdb->prefix : 'wp_';

		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true )
			&& 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' )
			&& 0 !== strpos( $meta->meta_key, 'closedpostboxes_' )
			&& 0 !== strpos( $meta->meta_key, 'metaboxhidden_' )
			&& 0 !== strpos( $meta->meta_key, 'manageedit-' )
			&& ! strstr( $meta->meta_key, $table_prefix )
			&& 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/**
	 * Method to create a new customer in the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Customer $customer Customer object.
	 *
	 * @throws WC_Data_Exception If unable to create new customer.
	 */
	public function create( &$customer ) {
		$id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() );

		if ( is_wp_error( $id ) ) {
			throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() );
		}

		$customer->set_id( $id );
		$this->update_user_meta( $customer );

		// Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email.
		$customer->set_password( '' );

		wp_update_user(
			apply_filters(
				'woocommerce_update_customer_args',
				array(
					'ID'           => $customer->get_id(),
					'role'         => $customer->get_role(),
					'display_name' => $customer->get_display_name(),
				),
				$customer
			)
		);
		$wp_user = new WP_User( $customer->get_id() );
		$customer->set_date_created( $wp_user->user_registered );
		$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
		$customer->save_meta_data();
		$customer->apply_changes();
		do_action( 'woocommerce_new_customer', $customer->get_id(), $customer );
	}

	/**
	 * Method to read a customer object.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @throws Exception If invalid customer.
	 */
	public function read( &$customer ) {
		$user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false;

		// User object is required.
		if ( ! $user_object || empty( $user_object->ID ) ) {
			throw new Exception( __( 'Invalid customer.', 'woocommerce' ) );
		}

		$customer_id = $customer->get_id();

		// Load meta but exclude deprecated props and parent keys.
		$user_meta = array_diff_key(
			array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ),
			array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ),
			array_change_key_case( (array) $user_object->data )
		);

		$customer->set_props( $user_meta );
		$customer->set_props(
			array(
				'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ),
				'email'              => $user_object->user_email,
				'username'           => $user_object->user_login,
				'display_name'       => $user_object->display_name,
				'date_created'       => $user_object->user_registered, // Mysql string in local format.
				'date_modified'      => get_user_meta( $customer_id, 'last_update', true ),
				'role'               => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer',
			)
		);
		$customer->read_meta_data();
		$customer->set_object_read( true );
		do_action( 'woocommerce_customer_loaded', $customer );
	}

	/**
	 * Updates a customer in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	public function update( &$customer ) {
		wp_update_user(
			apply_filters(
				'woocommerce_update_customer_args',
				array(
					'ID'           => $customer->get_id(),
					'user_email'   => $customer->get_email(),
					'display_name' => $customer->get_display_name(),
				),
				$customer
			)
		);

		// Only update password if a new one was set with set_password.
		if ( $customer->get_password() ) {
			wp_update_user(
				array(
					'ID'        => $customer->get_id(),
					'user_pass' => $customer->get_password(),
				)
			);
			$customer->set_password( '' );
		}

		$this->update_user_meta( $customer );
		$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
		$customer->save_meta_data();
		$customer->apply_changes();
		do_action( 'woocommerce_update_customer', $customer->get_id(), $customer );
	}

	/**
	 * Deletes a customer from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @param array       $args Array of args to pass to the delete method.
	 */
	public function delete( &$customer, $args = array() ) {
		if ( ! $customer->get_id() ) {
			return;
		}

		$args = wp_parse_args(
			$args,
			array(
				'reassign' => 0,
			)
		);

		$id = $customer->get_id();
		wp_delete_user( $id, $args['reassign'] );

		do_action( 'woocommerce_delete_customer', $id );
	}

	/**
	 * Helper method that updates all the meta for a customer. Used for update & create.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	private function update_user_meta( $customer ) {
		$updated_props = array();
		$changed_props = $customer->get_changes();

		$meta_key_to_props = array(
			'paying_customer' => 'is_paying_customer',
			'first_name'      => 'first_name',
			'last_name'       => 'last_name',
		);

		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			if ( ! array_key_exists( $prop, $changed_props ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		$billing_address_props = array(
			'billing_first_name' => 'billing_first_name',
			'billing_last_name'  => 'billing_last_name',
			'billing_company'    => 'billing_company',
			'billing_address_1'  => 'billing_address_1',
			'billing_address_2'  => 'billing_address_2',
			'billing_city'       => 'billing_city',
			'billing_state'      => 'billing_state',
			'billing_postcode'   => 'billing_postcode',
			'billing_country'    => 'billing_country',
			'billing_email'      => 'billing_email',
			'billing_phone'      => 'billing_phone',
		);

		foreach ( $billing_address_props as $meta_key => $prop ) {
			$prop_key = substr( $prop, 8 );

			if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		$shipping_address_props = array(
			'shipping_first_name' => 'shipping_first_name',
			'shipping_last_name'  => 'shipping_last_name',
			'shipping_company'    => 'shipping_company',
			'shipping_address_1'  => 'shipping_address_1',
			'shipping_address_2'  => 'shipping_address_2',
			'shipping_city'       => 'shipping_city',
			'shipping_state'      => 'shipping_state',
			'shipping_postcode'   => 'shipping_postcode',
			'shipping_country'    => 'shipping_country',
			'shipping_phone'      => 'shipping_phone',
		);

		foreach ( $shipping_address_props as $meta_key => $prop ) {
			$prop_key = substr( $prop, 9 );

			if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props );
	}

	/**
	 * Check if the usage of the custom orders table is enabled.
	 *
	 * @return bool
	 */
	private function is_cot_in_use(): bool {
		return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled();
	}

	/**
	 * Gets the customers last order.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return WC_Order|false
	 */
	public function get_last_order( &$customer ) {
		//phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
		/**
		 * Filters the id of the last order from a given customer.
		 *
		 * @param string @last_order_id The last order id as retrieved from the database.
		 * @param WC_Customer The customer whose last order id is being retrieved.
		 * @return string The actual last order id to use.
		 */
		$last_order_id = apply_filters(
			'woocommerce_customer_get_last_order',
			get_user_meta( $customer->get_id(), '_last_order', true ),
			$customer
		);
		//phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

		if ( '' === $last_order_id ) {
			global $wpdb;

			$order_statuses_sql = "( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )";

			//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			if ( $this->is_cot_in_use() ) {
				$sql           = $wpdb->prepare(
					'SELECT id FROM ' . OrdersTableDataStore::get_orders_table_name() . "
					WHERE customer_id = %d
					AND status in $order_statuses_sql
					ORDER BY id DESC
					LIMIT 1",
					$customer->get_id()
				);
				$last_order_id = $wpdb->get_var( $sql );
			} else {
				$last_order_id = $wpdb->get_var(
					"SELECT posts.ID
				FROM $wpdb->posts AS posts
				LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
				WHERE meta.meta_key = '_customer_user'
				AND   meta.meta_value = '" . esc_sql( $customer->get_id() ) . "'
				AND   posts.post_type = 'shop_order'
				AND   posts.post_status IN $order_statuses_sql
				ORDER BY posts.ID DESC
				LIMIT 1"
				);
			}
			//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			update_user_meta( $customer->get_id(), '_last_order', $last_order_id );
		}

		if ( ! $last_order_id ) {
			return false;
		}

		return wc_get_order( absint( $last_order_id ) );
	}

	/**
	 * Return the number of orders this customer has.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return integer
	 */
	public function get_order_count( &$customer ) {
		$count = apply_filters(
			'woocommerce_customer_get_order_count',
			get_user_meta( $customer->get_id(), '_order_count', true ),
			$customer
		);

		$order_statuses_sql = "( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )";

		if ( '' === $count ) {
			global $wpdb;

			//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			if ( $this->is_cot_in_use() ) {
				$sql   = $wpdb->prepare(
					'SELECT COUNT(id) FROM ' . OrdersTableDataStore::get_orders_table_name() . "
					WHERE customer_id = %d
					AND status in $order_statuses_sql",
					$customer->get_id()
				);
				$count = $wpdb->get_var( $sql );
			} else {
				$count = $wpdb->get_var(
					"SELECT COUNT(*)
				FROM $wpdb->posts as posts
				LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
				WHERE   meta.meta_key = '_customer_user'
				AND     posts.post_type = 'shop_order'
				AND     posts.post_status IN $order_statuses_sql
				AND     meta_value = '" . esc_sql( $customer->get_id() ) . "'"
				);
			}
			//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			update_user_meta( $customer->get_id(), '_order_count', $count );
		}

		return absint( $count );
	}

	/**
	 * Return how much money this customer has spent.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return float
	 */
	public function get_total_spent( &$customer ) {
		$spent = apply_filters(
			'woocommerce_customer_get_total_spent',
			get_user_meta( $customer->get_id(), '_money_spent', true ),
			$customer
		);

		if ( '' === $spent ) {
			global $wpdb;

			$statuses     = array_map( 'esc_sql', wc_get_is_paid_statuses() );
			$statuses_sql = "( 'wc-" . implode( "','wc-", $statuses ) . "' )";

			//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			if ( $this->is_cot_in_use() ) {
				$sql = $wpdb->prepare(
					'SELECT SUM(total_amount) FROM ' . OrdersTableDataStore::get_orders_table_name() . "
					WHERE customer_id = %d
					AND status in $statuses_sql",
					$customer->get_id()
				);
			} else {
				$sql = "SELECT SUM(meta2.meta_value)
					FROM $wpdb->posts as posts
					LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
					LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id
					WHERE   meta.meta_key       = '_customer_user'
					AND     meta.meta_value     = '" . esc_sql( $customer->get_id() ) . "'
					AND     posts.post_type     = 'shop_order'
					AND     posts.post_status   IN $statuses_sql
					AND     meta2.meta_key      = '_order_total'";
			}

			//phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
			/**
			 * Filters the SQL query used to get the combined total of all the orders from a given customer.
			 *
			 * @param string The SQL query to use.
			 * @param WC_Customer The customer to get the total spent for.
			 * @return string The actual SQL query to use.
			 */
			$sql = apply_filters( 'woocommerce_customer_get_total_spent_query', $sql, $customer );
			//phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

			$spent = $wpdb->get_var( $sql );
			//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			if ( ! $spent ) {
				$spent = 0;
			}
			update_user_meta( $customer->get_id(), '_money_spent', $spent );
		}

		return wc_format_decimal( $spent, 2 );
	}

	/**
	 * Search customers and return customer IDs.
	 *
	 * @param  string     $term Search term.
	 * @param  int|string $limit Limit search results.
	 * @since 3.0.7
	 *
	 * @return array
	 */
	public function search_customers( $term, $limit = '' ) {
		$results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit );
		if ( is_array( $results ) ) {
			return $results;
		}

		$query = new WP_User_Query(
			apply_filters(
				'woocommerce_customer_search_customers',
				array(
					'search'         => '*' . esc_attr( $term ) . '*',
					'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ),
					'fields'         => 'ID',
					'number'         => $limit,
				),
				$term,
				$limit,
				'main_query'
			)
		);

		$query2 = new WP_User_Query(
			apply_filters(
				'woocommerce_customer_search_customers',
				array(
					'fields'     => 'ID',
					'number'     => $limit,
					'meta_query' => array(
						'relation' => 'OR',
						array(
							'key'     => 'first_name',
							'value'   => $term,
							'compare' => 'LIKE',
						),
						array(
							'key'     => 'last_name',
							'value'   => $term,
							'compare' => 'LIKE',
						),
					),
				),
				$term,
				$limit,
				'meta_query'
			)
		);

		$results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) );

		if ( $limit && count( $results ) > $limit ) {
			$results = array_slice( $results, 0, $limit );
		}

		return $results;
	}

	/**
	 * Get all user ids who have `billing_email` set to any of the email passed in array.
	 *
	 * @param array $emails List of emails to check against.
	 *
	 * @return array
	 */
	public function get_user_ids_for_billing_email( $emails ) {
		$emails      = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) );
		$users_query = new WP_User_Query(
			array(
				'fields'     => 'ID',
				'meta_query' => array(
					array(
						'key'     => 'billing_email',
						'value'   => $emails,
						'compare' => 'IN',
					),
				),
			)
		);
		return array_unique( $users_query->get_results() );
	}
}
class-wc-customer-download-data-store.php000064400000040244151542600450014504 0ustar00<?php
/**
 * WC_Customer_Download_Data_Store class file.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Download Data Store.
 *
 * @version  3.0.0
 */
class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store_Interface {

	/**
	 * Names of the database fields for the download permissions table.
	 */
	const DOWNLOAD_PERMISSION_DB_FIELDS = array(
		'download_id',
		'product_id',
		'user_id',
		'user_email',
		'order_id',
		'order_key',
		'downloads_remaining',
		'access_granted',
		'download_count',
		'access_expires',
	);

	/**
	 * Create download permission for a user, from an array of data.
	 *
	 * @param array $data Data to create the permission for.
	 * @returns int The database id of the created permission, or false if the permission creation failed.
	 */
	public function create_from_data( $data ) {
		$data = array_intersect_key( $data, array_flip( self::DOWNLOAD_PERMISSION_DB_FIELDS ) );

		$id = $this->insert_new_download_permission( $data );

		do_action( 'woocommerce_grant_product_download_access', $data );

		return $id;
	}

	/**
	 * Create download permission for a user.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 */
	public function create( &$download ) {
		global $wpdb;

		// Always set a access granted date.
		if ( is_null( $download->get_access_granted( 'edit' ) ) ) {
			$download->set_access_granted( time() );
		}

		$data = array();
		foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) {
			$value                  = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' );
			$data[ $db_field_name ] = $value;
		}

		$inserted_id = $this->insert_new_download_permission( $data );
		if ( $inserted_id ) {
			$download->set_id( $inserted_id );
			$download->apply_changes();
		}

		do_action( 'woocommerce_grant_product_download_access', $data );
	}

	/**
	 * Create download permission for a user, from an array of data.
	 * Assumes that all the keys in the passed data are valid.
	 *
	 * @param array $data Data to create the permission for.
	 * @return int The database id of the created permission, or false if the permission creation failed.
	 */
	private function insert_new_download_permission( $data ) {
		global $wpdb;

		// Always set a access granted date.
		if ( ! isset( $data['access_granted'] ) ) {
			$data['access_granted'] = time();
		}

		$data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] );

		if ( isset( $data['access_expires'] ) ) {
			$data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] );
		}

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%d',
			'%s',
		);

		$result = $wpdb->insert(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			apply_filters( 'woocommerce_downloadable_file_permission_data', $data ),
			apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Adjust a date value to be inserted in the database.
	 *
	 * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes.
	 * @return string The date converted to 'Y-m-d' format.
	 * @throws Exception The passed value can't be converted to a date.
	 */
	private function adjust_date_for_db( $date ) {
		if ( 'WC_DateTime' === get_class( $date ) ) {
			$date = $date->getTimestamp();
		}

		$adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

		if ( $adjusted_date ) {
			return $adjusted_date;
		}

		$msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) );
		throw new Exception( $msg );
	}

	/**
	 * Method to read a download permission from the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 *
	 * @throws Exception Throw exception if invalid download is passed.
	 */
	public function read( &$download ) {
		global $wpdb;

		if ( ! $download->get_id() ) {
			throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
		}

		$download->set_defaults();
		$raw_download = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d",
				$download->get_id()
			)
		);

		if ( ! $raw_download ) {
			throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
		}

		$download->set_props(
			array(
				'download_id'         => $raw_download->download_id,
				'product_id'          => $raw_download->product_id,
				'user_id'             => $raw_download->user_id,
				'user_email'          => $raw_download->user_email,
				'order_id'            => $raw_download->order_id,
				'order_key'           => $raw_download->order_key,
				'downloads_remaining' => $raw_download->downloads_remaining,
				'access_granted'      => strtotime( $raw_download->access_granted ),
				'download_count'      => $raw_download->download_count,
				'access_expires'      => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ),
			)
		);
		$download->set_object_read( true );
	}

	/**
	 * Method to update a download in the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 */
	public function update( &$download ) {
		global $wpdb;

		$data = array(
			'download_id'         => $download->get_download_id( 'edit' ),
			'product_id'          => $download->get_product_id( 'edit' ),
			'user_id'             => $download->get_user_id( 'edit' ),
			'user_email'          => $download->get_user_email( 'edit' ),
			'order_id'            => $download->get_order_id( 'edit' ),
			'order_key'           => $download->get_order_key( 'edit' ),
			'downloads_remaining' => $download->get_downloads_remaining( 'edit' ),
			// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			'access_granted'      => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
			'download_count'      => $download->get_download_count( 'edit' ),
			// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			'access_expires'      => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%d',
			'%s',
		);

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			$data,
			array(
				'permission_id' => $download->get_id(),
			),
			$format
		);
		$download->apply_changes();
	}

	/**
	 * Method to delete a download permission from the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 * @param array                $args Array of args to pass to the delete method.
	 */
	public function delete( &$download, $args = array() ) {
		global $wpdb;

		$download_id = $download->get_id();
		$this->delete_by_id( $download_id );

		$download->set_id( 0 );
	}

	/**
	 * Method to delete a download permission from the database by ID.
	 *
	 * @param int $id permission_id of the download to be deleted.
	 */
	public function delete_by_id( $id ) {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE permission_id = %d",
				$id
			)
		);
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}wc_download_log
				WHERE permission_id = %d",
				$id
			)
		);
	}

	/**
	 * Delete download_log related to download permission via $field with value $value.
	 *
	 * @param string           $field Field used to query download permission table with.
	 * @param string|int|float $value Value to filter the field by.
	 *
	 * @return void
	 */
	private function delete_download_log_by_field_value( $field, $value ) {
		global $wpdb;

		$value_placeholder = '';
		if ( is_int( $value ) ) {
			$value_placeholder = '%d';
		} elseif ( is_string( $value ) ) {
			$value_placeholder = '%s';
		} elseif ( is_float( $value ) ) {
			$value_placeholder = '%f';
		} else {
			wc_doing_it_wrong( __METHOD__, __( 'Unsupported argument type provided as value.', 'woocommerce' ), '7.0' );
			// The `prepare` further down would fail if the placeholder was missing, so skip download log removal.
			return;
		}

		$query = "DELETE FROM {$wpdb->prefix}wc_download_log
					WHERE permission_id IN (
					    SELECT permission_id
					    FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
					    WHERE {$field} = {$value_placeholder}
					)";

		$wpdb->query(
			$wpdb->prepare( $query, $value ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		);
	}

	/**
	 * Method to delete a download permission from the database by order ID.
	 *
	 * @param int $id Order ID of the downloads that will be deleted.
	 */
	public function delete_by_order_id( $id ) {
		global $wpdb;
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$this->delete_download_log_by_field_value( 'order_id', $id );

		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE order_id = %d",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by download ID.
	 *
	 * @param int $id download_id of the downloads that will be deleted.
	 */
	public function delete_by_download_id( $id ) {
		global $wpdb;
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$this->delete_download_log_by_field_value( 'download_id', $id );

		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE download_id = %s",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by user ID.
	 *
	 * @since 3.4.0
	 * @param int $id user ID of the downloads that will be deleted.
	 * @return bool True if deleted rows.
	 */
	public function delete_by_user_id( $id ) {
		global $wpdb;
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$this->delete_download_log_by_field_value( 'user_id', $id );

		return (bool) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE user_id = %d",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by user email.
	 *
	 * @since 3.4.0
	 * @param string $email email of the downloads that will be deleted.
	 * @return bool True if deleted rows.
	 */
	public function delete_by_user_email( $email ) {
		global $wpdb;
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$this->delete_download_log_by_field_value( 'user_email', $email );

		return (bool) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE user_email = %s",
				$email
			)
		);
	}

	/**
	 * Get a download object.
	 *
	 * @param  array $data From the DB.
	 * @return WC_Customer_Download
	 */
	private function get_download( $data ) {
		return new WC_Customer_Download( $data );
	}

	/**
	 * Get array of download ids by specified args.
	 *
	 * @param  array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separated list of fields (for example: 'order_id,user_id,user_email').
	 * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return'].
	 */
	public function get_downloads( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'user_email'  => '',
				'user_id'     => '',
				'order_id'    => '',
				'order_key'   => '',
				'product_id'  => '',
				'download_id' => '',
				'orderby'     => 'permission_id',
				'order'       => 'ASC',
				'limit'       => -1,
				'page'        => 1,
				'return'      => 'objects',
			)
		);

		$valid_fields       = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' );
		$get_results_output = ARRAY_A;

		if ( 'ids' === $args['return'] ) {
			$fields = 'permission_id';
		} elseif ( 'objects' === $args['return'] ) {
			$fields             = '*';
			$get_results_output = OBJECT;
		} else {
			$fields = explode( ',', (string) $args['return'] );
			$fields = implode( ', ', array_intersect( $fields, $valid_fields ) );
		}

		$query   = array();
		$query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1";

		if ( $args['user_email'] ) {
			$query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) );
		}

		if ( $args['user_id'] ) {
			$query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) );
		}

		if ( $args['order_id'] ) {
			$query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] );
		}

		if ( $args['order_key'] ) {
			$query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] );
		}

		if ( $args['product_id'] ) {
			$query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] );
		}

		if ( $args['download_id'] ) {
			$query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] );
		}

		$orderby     = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id';
		$order       = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
		$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
		$query[]     = "ORDER BY {$orderby_sql}";

		if ( 0 < $args['limit'] ) {
			$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results( implode( ' ', $query ), $get_results_output );

		switch ( $args['return'] ) {
			case 'ids':
				return wp_list_pluck( $results, 'permission_id' );
			case 'objects':
				return array_map( array( $this, 'get_download' ), $results );
			default:
				return $results;
		}
	}

	/**
	 * Update download ids if the hash changes.
	 *
	 * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash.
	 *
	 * @param  int    $product_id Product ID.
	 * @param  string $old_id Old download_id.
	 * @param  string $new_id New download_id.
	 */
	public function update_download_id( $product_id, $old_id, $new_id ) {
		global $wpdb;

		wc_deprecated_function( __METHOD__, '3.3' );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			array(
				'download_id' => $new_id,
			),
			array(
				'download_id' => $old_id,
				'product_id'  => $product_id,
			)
		);
	}

	/**
	 * Get a customers downloads.
	 *
	 * @param  int $customer_id Customer ID.
	 * @return array
	 */
	public function get_downloads_for_customer( $customer_id ) {
		global $wpdb;

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions
				WHERE user_id = %d
				AND permissions.order_id > 0
				AND
					(
						permissions.downloads_remaining > 0
						OR permissions.downloads_remaining = ''
					)
				AND
					(
						permissions.access_expires IS NULL
						OR permissions.access_expires >= %s
						OR permissions.access_expires = '0000-00-00 00:00:00'
					)
				ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;",
				$customer_id,
				date( 'Y-m-d', current_time( 'timestamp' ) )  // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			)
		);
	}

	/**
	 * Update user prop for downloads based on order id.
	 *
	 * @param  int    $order_id Order ID.
	 * @param  int    $customer_id Customer ID.
	 * @param  string $email Customer email address.
	 */
	public function update_user_by_order_id( $order_id, $customer_id, $email ) {
		global $wpdb;
		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			array(
				'user_id'    => $customer_id,
				'user_email' => $email,
			),
			array(
				'order_id' => $order_id,
			),
			array(
				'%d',
				'%s',
			),
			array(
				'%d',
			)
		);
	}
}
class-wc-customer-download-log-data-store.php000064400000015342151542600450015264 0ustar00<?php
/**
 * Class WC_Customer_Download_Log_Data_Store file.
 *
 * @version  3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Customer_Download_Log_Data_Store class.
 */
class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Data_Store_Interface {

	// Table name for download logs.
	const WC_DOWNLOAD_LOG_TABLE = 'wc_download_log';

	/**
	 * Get the table name for download logs.
	 *
	 * @return string
	 */
	public static function get_table_name() {
		return self::WC_DOWNLOAD_LOG_TABLE;
	}

	/**
	 * Create download log entry.
	 *
	 * @param WC_Customer_Download_Log $download_log Customer download log object.
	 */
	public function create( WC_Customer_Download_Log &$download_log ) {
		global $wpdb;

		// Always set a timestamp.
		if ( is_null( $download_log->get_timestamp( 'edit' ) ) ) {
			$download_log->set_timestamp( time() );
		}

		$data = array(
			'timestamp'       => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
			'permission_id'   => $download_log->get_permission_id( 'edit' ),
			'user_id'         => $download_log->get_user_id( 'edit' ),
			'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
		);

		$result = $wpdb->insert(
			$wpdb->prefix . self::get_table_name(),
			apply_filters( 'woocommerce_downloadable_product_download_log_insert_data', $data ),
			apply_filters( 'woocommerce_downloadable_product_download_log_insert_format', $format, $data )
		);

		do_action( 'woocommerce_downloadable_product_download_log_insert', $data );

		if ( $result ) {
			$download_log->set_id( $wpdb->insert_id );
			$download_log->apply_changes();
		} else {
			wp_die( esc_html__( 'Unable to insert download log entry in database.', 'woocommerce' ) );
		}
	}

	/**
	 * Method to read a download log from the database.
	 *
	 * @param WC_Customer_Download_Log $download_log Download log object.
	 * @throws Exception Exception when read is not possible.
	 */
	public function read( &$download_log ) {
		global $wpdb;

		$download_log->set_defaults();

		// Ensure we have an id to pull from the DB.
		if ( ! $download_log->get_id() ) {
			throw new Exception( __( 'Invalid download log: no ID.', 'woocommerce' ) );
		}

		$table = $wpdb->prefix . self::get_table_name();

		// Query the DB for the download log.
		$raw_download_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE download_log_id = %d", $download_log->get_id() ) ); // WPCS: unprepared SQL ok.

		if ( ! $raw_download_log ) {
			throw new Exception( __( 'Invalid download log: not found.', 'woocommerce' ) );
		}

		$download_log->set_props(
			array(
				'timestamp'       => strtotime( $raw_download_log->timestamp ),
				'permission_id'   => $raw_download_log->permission_id,
				'user_id'         => $raw_download_log->user_id,
				'user_ip_address' => $raw_download_log->user_ip_address,
			)
		);

		$download_log->set_object_read( true );
	}

	/**
	 * Method to update a download log in the database.
	 *
	 * @param WC_Customer_Download_Log $download_log Download log object.
	 */
	public function update( &$download_log ) {
		global $wpdb;

		$data = array(
			'timestamp'       => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
			'permission_id'   => $download_log->get_permission_id( 'edit' ),
			'user_id'         => $download_log->get_user_id( 'edit' ),
			'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
		);

		$wpdb->update(
			$wpdb->prefix . self::get_table_name(),
			$data,
			array(
				'download_log_id' => $download_log->get_id(),
			),
			$format
		);
		$download_log->apply_changes();
	}

	/**
	 * Get a download log object.
	 *
	 * @param  array $data From the DB.
	 * @return WC_Customer_Download_Log
	 */
	private function get_download_log( $data ) {
		return new WC_Customer_Download_Log( $data );
	}

	/**
	 * Get array of download log ids by specified args.
	 *
	 * @param  array $args Arguments to define download logs to retrieve.
	 * @return array
	 */
	public function get_download_logs( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'permission_id'   => '',
				'user_id'         => '',
				'user_ip_address' => '',
				'orderby'         => 'download_log_id',
				'order'           => 'ASC',
				'limit'           => -1,
				'page'            => 1,
				'return'          => 'objects',
			)
		);

		$query   = array();
		$table   = $wpdb->prefix . self::get_table_name();
		$query[] = "SELECT * FROM {$table} WHERE 1=1";

		if ( $args['permission_id'] ) {
			$query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] );
		}

		if ( $args['user_id'] ) {
			$query[] = $wpdb->prepare( 'AND user_id = %d', $args['user_id'] );
		}

		if ( $args['user_ip_address'] ) {
			$query[] = $wpdb->prepare( 'AND user_ip_address = %s', $args['user_ip_address'] );
		}

		$allowed_orders = array( 'download_log_id', 'timestamp', 'permission_id', 'user_id' );
		$orderby        = in_array( $args['orderby'], $allowed_orders, true ) ? $args['orderby'] : 'download_log_id';
		$order          = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
		$orderby_sql    = sanitize_sql_orderby( "{$orderby} {$order}" );
		$query[]        = "ORDER BY {$orderby_sql}";

		if ( 0 < $args['limit'] ) {
			$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
		}

		$raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok.

		switch ( $args['return'] ) {
			case 'ids':
				return wp_list_pluck( $raw_download_logs, 'download_log_id' );
			default:
				return array_map( array( $this, 'get_download_log' ), $raw_download_logs );
		}
	}

	/**
	 * Get download logs for a given download permission.
	 *
	 * @param int $permission_id Permission to get logs for.
	 * @return array
	 */
	public function get_download_logs_for_permission( $permission_id ) {
		// If no permission_id is passed, return an empty array.
		if ( empty( $permission_id ) ) {
			return array();
		}

		return $this->get_download_logs(
			array(
				'permission_id' => $permission_id,
			)
		);
	}

	/**
	 * Method to delete download logs for a given permission ID.
	 *
	 * @since 3.4.0
	 * @param int $id download_id of the downloads that will be deleted.
	 */
	public function delete_by_permission_id( $id ) {
		global $wpdb;
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) );
		// Delete related records in wc_download_log (aka ON DELETE CASCADE).
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}wc_download_log WHERE permission_id = %d", $id ) );
	}
}
class-wc-data-store-wp.php000064400000047422151542600450011471 0ustar00<?php
/**
 * Shared logic for WP based data.
 * Contains functions like meta handling for all default data stores.
 * Your own data store doesn't need to use WC_Data_Store_WP -- you can write
 * your own meta handling functions.
 *
 * @version 3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Data_Store_WP class.
 */
class WC_Data_Store_WP {

	/**
	 * Meta type. This should match up with
	 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
	 * WP defines 'post', 'user', 'comment', and 'term'.
	 *
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * This only needs set if you are using a custom metadata type (for example payment tokens.
	 * This should be the name of the field your table uses for associating meta with objects.
	 * For example, in payment_tokenmeta, this would be payment_token_id.
	 *
	 * @var string
	 */
	protected $object_id_field_for_meta = '';

	/**
	 * Data stored in meta keys, but not considered "meta" for an object.
	 *
	 * @since 3.0.0
	 *
	 * @var array
	 */
	protected $internal_meta_keys = array();

	/**
	 * Meta data which should exist in the DB, even if empty.
	 *
	 * @since 3.6.0
	 *
	 * @var array
	 */
	protected $must_exist_meta_keys = array();

	/**
	 * Get and store terms from a taxonomy.
	 *
	 * @since  3.0.0
	 * @param  WC_Data|integer $object WC_Data object or object ID.
	 * @param  string          $taxonomy Taxonomy name e.g. product_cat.
	 * @return array of terms
	 */
	protected function get_term_ids( $object, $taxonomy ) {
		if ( is_numeric( $object ) ) {
			$object_id = $object;
		} else {
			$object_id = $object->get_id();
		}
		$terms = get_the_terms( $object_id, $taxonomy );
		if ( false === $terms || is_wp_error( $terms ) ) {
			return array();
		}
		return wp_list_pluck( $terms, 'term_id' );
	}

	/**
	 * Returns an array of meta for an object.
	 *
	 * @since  3.0.0
	 * @param  WC_Data $object WC_Data object.
	 * @return array
	 */
	public function read_meta( &$object ) {
		global $wpdb;
		$db_info       = $this->get_db_info();
		$raw_meta_data = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value
				FROM {$db_info['table']}
				WHERE {$db_info['object_id_field']} = %d
				ORDER BY {$db_info['meta_id_field']}",
				// phpcs:enable
				$object->get_id()
			)
		);
		return $this->filter_raw_meta_data( $object, $raw_meta_data );
	}

	/**
	 * Helper method to filter internal meta keys from all meta data rows for the object.
	 *
	 * @since 4.7.0
	 *
	 * @param WC_Data $object        WC_Data object.
	 * @param array   $raw_meta_data Array of std object of meta data to be filtered.
	 *
	 * @return mixed|void
	 */
	public function filter_raw_meta_data( &$object, $raw_meta_data ) {
		$this->internal_meta_keys = array_unique(
			array_merge(
				array_map(
					array( $this, 'prefix_key' ),
					$object->get_data_keys()
				),
				$this->internal_meta_keys
			)
		);
		$meta_data                = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
		return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
	}

	/**
	 * Deletes meta based on meta ID.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing at least ->id).
	 */
	public function delete_meta( &$object, $meta ) {
		delete_metadata_by_mid( $this->meta_type, $meta->id );
	}

	/**
	 * Add new piece of meta.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->key and ->value).
	 * @return int meta ID
	 */
	public function add_meta( &$object, $meta ) {
		return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false );
	}

	/**
	 * Update meta.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->id, ->key and ->value).
	 */
	public function update_meta( &$object, $meta ) {
		update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key );
	}

	/**
	 * Table structure is slightly different between meta types, this function will return what we need to know.
	 *
	 * @since  3.0.0
	 * @return array Array elements: table, object_id_field, meta_id_field
	 */
	protected function get_db_info() {
		global $wpdb;

		$meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
		$table         = $wpdb->prefix;

		// If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
		if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ), true ) ) {
			$table .= 'woocommerce_';
		}

		$table          .= $this->meta_type . 'meta';
		$object_id_field = $this->meta_type . '_id';

		// Figure out our field names.
		if ( 'user' === $this->meta_type ) {
			$meta_id_field = 'umeta_id';
			$table         = $wpdb->usermeta;
		}

		if ( ! empty( $this->object_id_field_for_meta ) ) {
			$object_id_field = $this->object_id_field_for_meta;
		}

		return array(
			'table'           => $table,
			'object_id_field' => $object_id_field,
			'meta_id_field'   => $meta_id_field,
		);
	}

	/**
	 * Internal meta keys we don't want exposed as part of meta_data. This is in
	 * addition to all data props with _ prefix.
	 *
	 * @since 2.6.0
	 *
	 * @param string $key Prefix to be added to meta keys.
	 * @return string
	 */
	protected function prefix_key( $key ) {
		return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
	}

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object to check if it should be excluded or not.
	 * @return bool
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/**
	 * Gets a list of props and meta keys that need updated based on change state
	 * or if they are present in the database or not.
	 *
	 * @param  WC_Data $object              The WP_Data object (WC_Coupon for coupons, etc).
	 * @param  array   $meta_key_to_props   A mapping of meta keys => prop names.
	 * @param  string  $meta_type           The internal WP meta type (post, user, etc).
	 * @return array                        A mapping of meta keys => prop names, filtered by ones that should be updated.
	 */
	protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) {
		$props_to_update = array();
		$changed_props   = $object->get_changes();

		// Props should be updated if they are a part of the $changed array or don't exist yet.
		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) {
				$props_to_update[ $meta_key ] = $prop;
			}
		}

		return $props_to_update;
	}

	/**
	 * Update meta data in, or delete it from, the database.
	 *
	 * Avoids storing meta when it's either an empty string or empty array.
	 * Other empty values such as numeric 0 and null should still be stored.
	 * Data-stores can force meta to exist using `must_exist_meta_keys`.
	 *
	 * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
	 *
	 * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
	 * @param string  $meta_key Meta key to update.
	 * @param mixed   $meta_value Value to save.
	 *
	 * @since 3.6.0 Added to prevent empty meta being stored unless required.
	 *
	 * @return bool True if updated/deleted.
	 */
	protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
		if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
			$updated = delete_post_meta( $object->get_id(), $meta_key );
		} else {
			$updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
		}

		return (bool) $updated;
	}

	/**
	 * Get valid WP_Query args from a WC_Object_Query's query variables.
	 *
	 * @since 3.1.0
	 * @param array $query_vars query vars from a WC_Object_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		$skipped_values = array( '', array(), null );
		$wp_query_args  = array(
			'errors'     => array(),
			'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		);

		foreach ( $query_vars as $key => $value ) {
			if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) {
				continue;
			}

			// Build meta queries out of vars that are stored in internal meta keys.
			if ( in_array( '_' . $key, $this->internal_meta_keys, true ) ) {
				// Check for existing values if wildcard is used.
				if ( '*' === $value ) {
					$wp_query_args['meta_query'][] = array(
						array(
							'key'     => '_' . $key,
							'compare' => 'EXISTS',
						),
						array(
							'key'     => '_' . $key,
							'value'   => '',
							'compare' => '!=',
						),
					);
				} else {
					$wp_query_args['meta_query'][] = array(
						'key'     => '_' . $key,
						'value'   => $value,
						'compare' => is_array( $value ) ? 'IN' : '=',
					);
				}
			} else { // Other vars get mapped to wp_query args or just left alone.
				$key_mapping = array(
					'parent'         => 'post_parent',
					'parent_exclude' => 'post_parent__not_in',
					'exclude'        => 'post__not_in',
					'limit'          => 'posts_per_page',
					'type'           => 'post_type',
					'return'         => 'fields',
				);

				if ( isset( $key_mapping[ $key ] ) ) {
					$wp_query_args[ $key_mapping[ $key ] ] = $value;
				} else {
					$wp_query_args[ $key ] = $value;
				}
			}
		}

		return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars );
	}

	/**
	 * Map a valid date query var to WP_Query arguments.
	 * Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators.
	 * Also accepts a WC_DateTime object.
	 *
	 * @since 3.2.0
	 * @param mixed  $query_var A valid date format.
	 * @param string $key meta or db column key.
	 * @param array  $wp_query_args WP_Query args.
	 * @return array Modified $wp_query_args
	 */
	public function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) {
		$query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';
		$valid_operators   = array( '>', '>=', '=', '<=', '<', '...' );

		// YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision.
		$precision = 'second';

		$dates    = array();
		$operator = '=';

		try {
			// Specific time query with a WC_DateTime.
			if ( is_a( $query_var, 'WC_DateTime' ) ) {
				$dates[] = $query_var;
			} elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp.
				$dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) );
			} elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates.
				if ( ! empty( $sections[1] ) ) {
					$dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] );
				}

				$operator = in_array( $sections[2], $valid_operators, true ) ? $sections[2] : '';
				$dates[]  = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] );

				if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) {
					$precision = 'day';
				}
			} else { // Specific time query with a string.
				$dates[]   = wc_string_to_datetime( $query_var );
				$precision = 'day';
			}
		} catch ( Exception $e ) {
			return $wp_query_args;
		}

		// Check for valid inputs.
		if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) {
			return $wp_query_args;
		}

		// Build date query for 'post_date' or 'post_modified' keys.
		if ( 'post_date' === $key || 'post_modified' === $key ) {
			if ( ! isset( $wp_query_args['date_query'] ) ) {
				$wp_query_args['date_query'] = array();
			}

			$query_arg = array(
				'column'    => 'day' === $precision ? $key : $key . '_gmt',
				'inclusive' => '>' !== $operator && '<' !== $operator,
			);

			// Add 'before'/'after' query args.
			$comparisons = array();
			if ( '>' === $operator || '>=' === $operator || '...' === $operator ) {
				$comparisons[] = 'after';
			}
			if ( '<' === $operator || '<=' === $operator || '...' === $operator ) {
				$comparisons[] = 'before';
			}

			foreach ( $comparisons as $index => $comparison ) {
				if ( 'day' === $precision ) {
					/**
					 * WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and
					 * 'after' string query, so we have to use the array format in 'day' precision.
					 *
					 * @see https://core.trac.wordpress.org/ticket/29908
					 */
					$query_arg[ $comparison ]['year']  = $dates[ $index ]->date( 'Y' );
					$query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' );
					$query_arg[ $comparison ]['day']   = $dates[ $index ]->date( 'j' );
				} else {
					/**
					 * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries,
					 * so we have to use a string query.
					 */
					$query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() );
				}
			}

			if ( empty( $comparisons ) ) {
				$query_arg['year']  = $dates[0]->date( 'Y' );
				$query_arg['month'] = $dates[0]->date( 'n' );
				$query_arg['day']   = $dates[0]->date( 'j' );
				if ( 'second' === $precision ) {
					$query_arg['hour']   = $dates[0]->date( 'H' );
					$query_arg['minute'] = $dates[0]->date( 'i' );
					$query_arg['second'] = $dates[0]->date( 's' );
				}
			}
			$wp_query_args['date_query'][] = $query_arg;
			return $wp_query_args;
		}

		// Build meta query for unrecognized keys.
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		// Meta dates are stored as timestamps in the db.
		// Check against beginning/end-of-day timestamps when using 'day' precision.
		if ( 'day' === $precision ) {
			$start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) );
			$end_timestamp   = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) );
			switch ( $operator ) {
				case '>':
				case '<=':
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $end_timestamp,
						'compare' => $operator,
					);
					break;
				case '<':
				case '>=':
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $start_timestamp,
						'compare' => $operator,
					);
					break;
				default:
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $start_timestamp,
						'compare' => '>=',
					);
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $end_timestamp,
						'compare' => '<=',
					);
			}
		} else {
			if ( '...' !== $operator ) {
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[0]->getTimestamp(),
					'compare' => $operator,
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[0]->getTimestamp(),
					'compare' => '>=',
				);
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[1]->getTimestamp(),
					'compare' => '<=',
				);
			}
		}

		return $wp_query_args;
	}

	/**
	 * Return list of internal meta keys.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_internal_meta_keys() {
		return $this->internal_meta_keys;
	}

	/**
	 * Check if the terms are suitable for searching.
	 *
	 * Uses an array of stopwords (terms) that are excluded from the separate
	 * term matching when searching for posts. The list of English stopwords is
	 * the approximate search engines list, and is translatable.
	 *
	 * @since 3.4.0
	 * @param array $terms Terms to check.
	 * @return array Terms that are not stopwords.
	 */
	protected function get_valid_search_terms( $terms ) {
		$valid_terms = array();
		$stopwords   = $this->get_search_stopwords();

		foreach ( $terms as $term ) {
			// keep before/after spaces when term is for exact match, otherwise trim quotes and spaces.
			if ( preg_match( '/^".+"$/', $term ) ) {
				$term = trim( $term, "\"'" );
			} else {
				$term = trim( $term, "\"' " );
			}

			// Avoid single A-Z and single dashes.
			if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
				continue;
			}

			if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) {
				continue;
			}

			$valid_terms[] = $term;
		}

		return $valid_terms;
	}

	/**
	 * Retrieve stopwords used when parsing search terms.
	 *
	 * @since 3.4.0
	 * @return array Stopwords.
	 */
	protected function get_search_stopwords() {
		// 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.
		$stopwords = array_map(
			'wc_strtolower',
			array_map(
				'trim',
				explode(
					',',
					_x(
						'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',
						'Comma-separated list of search stopwords in your language',
						'woocommerce'
					)
				)
			)
		);

		return apply_filters( 'wp_search_stopwords', $stopwords );
	}

	/**
	 * Get data to save to a lookup table.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 * @return array
	 */
	protected function get_data_for_lookup_table( $id, $table ) {
		return array();
	}

	/**
	 * Get primary key name for lookup table.
	 *
	 * @since 3.6.0
	 * @param string $table Lookup table name.
	 * @return string
	 */
	protected function get_primary_key_for_lookup_table( $table ) {
		return '';
	}

	/**
	 * Update a lookup table for an object.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 *
	 * @return NULL
	 */
	protected function update_lookup_table( $id, $table ) {
		global $wpdb;

		$id    = absint( $id );
		$table = sanitize_key( $table );

		if ( empty( $id ) || empty( $table ) ) {
			return false;
		}

		$existing_data = wp_cache_get( 'lookup_table', 'object_' . $id );
		$update_data   = $this->get_data_for_lookup_table( $id, $table );

		if ( ! empty( $update_data ) && $update_data !== $existing_data ) {
			$wpdb->replace(
				$wpdb->$table,
				$update_data
			);
			wp_cache_set( 'lookup_table', $update_data, 'object_' . $id );
		}
	}

	/**
	 * Delete lookup table data for an ID.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 */
	public function delete_from_lookup_table( $id, $table ) {
		global $wpdb;

		$id    = absint( $id );
		$table = sanitize_key( $table );

		if ( empty( $id ) || empty( $table ) ) {
			return false;
		}

		$pk = $this->get_primary_key_for_lookup_table( $table );

		$wpdb->delete(
			$wpdb->$table,
			array(
				$pk => $id,
			)
		);
		wp_cache_delete( 'lookup_table', 'object_' . $id );
	}

	/**
	 * Converts a WP post date string into a timestamp.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $time_string The WP post date string.
	 * @return int|null The date string converted to a timestamp or null.
	 */
	protected function string_to_timestamp( $time_string ) {
		return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
	}
}
class-wc-order-data-store-cpt.php000064400000117720151542600450012741 0ustar00<?php
/**
 * WC_Order_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_customer_user',
		'_order_key',
		'_order_currency',
		'_billing_first_name',
		'_billing_last_name',
		'_billing_company',
		'_billing_address_1',
		'_billing_address_2',
		'_billing_city',
		'_billing_state',
		'_billing_postcode',
		'_billing_country',
		'_billing_email',
		'_billing_phone',
		'_shipping_first_name',
		'_shipping_last_name',
		'_shipping_company',
		'_shipping_address_1',
		'_shipping_address_2',
		'_shipping_city',
		'_shipping_state',
		'_shipping_postcode',
		'_shipping_country',
		'_shipping_phone',
		'_completed_date',
		'_paid_date',
		'_edit_lock',
		'_edit_last',
		'_cart_discount',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_payment_method',
		'_payment_method_title',
		'_transaction_id',
		'_customer_ip_address',
		'_customer_user_agent',
		'_created_via',
		'_order_version',
		'_prices_include_tax',
		'_date_completed',
		'_date_paid',
		'_payment_tokens',
		'_billing_address_index',
		'_shipping_address_index',
		'_recorded_sales',
		'_recorded_coupon_usage_counts',
		'_download_permissions_granted',
		'_order_stock_reduced',
		'_new_order_email_sent',
	);

	/**
	 * Custom setters for props. Add key here if it has corresponding set_ and get_ method present.
	 *
	 * @var string[]
	 */
	protected $internal_data_store_key_getters = array(
		'_download_permissions_granted' => 'download_permissions_granted',
		'_recorded_sales'               => 'recorded_sales',
		'_recorded_coupon_usage_counts' => 'recorded_coupon_usage_counts',
		'_order_stock_reduced'          => 'order_stock_reduced',
		'_new_order_email_sent'         => 'new_order_email_sent',
	);

	/**
	 * Method to create a new order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function create( &$order ) {
		if ( '' === $order->get_order_key() ) {
			$order->set_order_key( wc_generate_order_key() );
		}
		parent::create( $order );
		do_action( 'woocommerce_new_order', $order->get_id(), $order );
	}

	/**
	 * Read order data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order $order Order object.
	 * @param object   $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$order, $post_object ) {
		parent::read_order_data( $order, $post_object );
		$id             = $order->get_id();
		$date_completed = get_post_meta( $id, '_date_completed', true );
		$date_paid      = get_post_meta( $id, '_date_paid', true );

		if ( ! $date_completed ) {
			$date_completed = get_post_meta( $id, '_completed_date', true );
		}

		if ( ! $date_paid ) {
			$date_paid = get_post_meta( $id, '_paid_date', true );
		}

		$order->set_props(
			array(
				'order_key'                    => get_post_meta( $id, '_order_key', true ),
				'customer_id'                  => get_post_meta( $id, '_customer_user', true ),
				'billing_first_name'           => get_post_meta( $id, '_billing_first_name', true ),
				'billing_last_name'            => get_post_meta( $id, '_billing_last_name', true ),
				'billing_company'              => get_post_meta( $id, '_billing_company', true ),
				'billing_address_1'            => get_post_meta( $id, '_billing_address_1', true ),
				'billing_address_2'            => get_post_meta( $id, '_billing_address_2', true ),
				'billing_city'                 => get_post_meta( $id, '_billing_city', true ),
				'billing_state'                => get_post_meta( $id, '_billing_state', true ),
				'billing_postcode'             => get_post_meta( $id, '_billing_postcode', true ),
				'billing_country'              => get_post_meta( $id, '_billing_country', true ),
				'billing_email'                => get_post_meta( $id, '_billing_email', true ),
				'billing_phone'                => get_post_meta( $id, '_billing_phone', true ),
				'shipping_first_name'          => get_post_meta( $id, '_shipping_first_name', true ),
				'shipping_last_name'           => get_post_meta( $id, '_shipping_last_name', true ),
				'shipping_company'             => get_post_meta( $id, '_shipping_company', true ),
				'shipping_address_1'           => get_post_meta( $id, '_shipping_address_1', true ),
				'shipping_address_2'           => get_post_meta( $id, '_shipping_address_2', true ),
				'shipping_city'                => get_post_meta( $id, '_shipping_city', true ),
				'shipping_state'               => get_post_meta( $id, '_shipping_state', true ),
				'shipping_postcode'            => get_post_meta( $id, '_shipping_postcode', true ),
				'shipping_country'             => get_post_meta( $id, '_shipping_country', true ),
				'shipping_phone'               => get_post_meta( $id, '_shipping_phone', true ),
				'payment_method'               => get_post_meta( $id, '_payment_method', true ),
				'payment_method_title'         => get_post_meta( $id, '_payment_method_title', true ),
				'transaction_id'               => get_post_meta( $id, '_transaction_id', true ),
				'customer_ip_address'          => get_post_meta( $id, '_customer_ip_address', true ),
				'customer_user_agent'          => get_post_meta( $id, '_customer_user_agent', true ),
				'created_via'                  => get_post_meta( $id, '_created_via', true ),
				'date_completed'               => $date_completed,
				'date_paid'                    => $date_paid,
				'cart_hash'                    => get_post_meta( $id, '_cart_hash', true ),
				'customer_note'                => $post_object->post_excerpt,

				// Operational data props.
				'order_stock_reduced'          => get_post_meta( $id, '_order_stock_reduced', true ),
				'download_permissions_granted' => get_post_meta( $id, '_download_permissions_granted', true ),
				'new_order_email_sent'         => get_post_meta( $id, '_new_order_email_sent', true ),
				'recorded_sales'               => wc_string_to_bool( get_post_meta( $id, '_recorded_sales', true ) ),
				'recorded_coupon_usage_counts' => get_post_meta( $id, '_recorded_coupon_usage_counts', true ),
			)
		);
	}

	/**
	 * Method to update an order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function update( &$order ) {
		// Before updating, ensure date paid is set if missing.
		if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) {
			$order->set_date_paid( $order->get_date_created( 'edit' ) );
		}

		// Also grab the current status so we can compare.
		$previous_status = get_post_status( $order->get_id() );

		// Update the order.
		parent::update( $order );

		// Fire a hook depending on the status - this should be considered a creation if it was previously draft status.
		$new_status = $order->get_status( 'edit' );

		if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) {
			do_action( 'woocommerce_new_order', $order->get_id(), $order );
		} else {
			do_action( 'woocommerce_update_order', $order->get_id(), $order );
		}
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$order ) {
		$updated_props     = array();
		$id                = $order->get_id();
		$meta_key_to_props = array(
			'_order_key'                    => 'order_key',
			'_customer_user'                => 'customer_id',
			'_payment_method'               => 'payment_method',
			'_payment_method_title'         => 'payment_method_title',
			'_transaction_id'               => 'transaction_id',
			'_customer_ip_address'          => 'customer_ip_address',
			'_customer_user_agent'          => 'customer_user_agent',
			'_created_via'                  => 'created_via',
			'_date_completed'               => 'date_completed',
			'_date_paid'                    => 'date_paid',
			'_cart_hash'                    => 'cart_hash',
			'_download_permissions_granted' => 'download_permissions_granted',
			'_recorded_sales'               => 'recorded_sales',
			'_recorded_coupon_usage_counts' => 'recorded_coupon_usage_counts',
			'_new_order_email_sent'         => 'new_order_email_sent',
			'_order_stock_reduced'          => 'order_stock_reduced',
		);

		$props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $order->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'date_paid':
				case 'date_completed':
					$value = ! is_null( $value ) ? $value->getTimestamp() : '';
					break;
				case 'download_permissions_granted':
				case 'recorded_sales':
				case 'recorded_coupon_usage_counts':
				case 'order_stock_reduced':
					if ( is_null( $value ) || '' === $value ) {
						break;
					}
					$value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value;
					break;
				case 'new_order_email_sent':
					if ( is_null( $value ) || '' === $value ) {
						break;
					}
					$value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value;
					$value = 'yes' === $value ? 'true' : 'false'; // For backward compatibility, we store as true/false in DB.
					break;
			}

			// We want to persist internal data store keys as 'yes' or 'no' if they are boolean to maintain compatibility.
			if ( is_bool( $value ) && in_array( $prop, array_values( $this->internal_data_store_key_getters ), true ) ) {
				$value = wc_bool_to_string( $value );
			}

			$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

			if ( $updated ) {
				$updated_props[] = $prop;
			}
		}

		$address_props = array(
			'billing'  => array(
				'_billing_first_name' => 'billing_first_name',
				'_billing_last_name'  => 'billing_last_name',
				'_billing_company'    => 'billing_company',
				'_billing_address_1'  => 'billing_address_1',
				'_billing_address_2'  => 'billing_address_2',
				'_billing_city'       => 'billing_city',
				'_billing_state'      => 'billing_state',
				'_billing_postcode'   => 'billing_postcode',
				'_billing_country'    => 'billing_country',
				'_billing_email'      => 'billing_email',
				'_billing_phone'      => 'billing_phone',
			),
			'shipping' => array(
				'_shipping_first_name' => 'shipping_first_name',
				'_shipping_last_name'  => 'shipping_last_name',
				'_shipping_company'    => 'shipping_company',
				'_shipping_address_1'  => 'shipping_address_1',
				'_shipping_address_2'  => 'shipping_address_2',
				'_shipping_city'       => 'shipping_city',
				'_shipping_state'      => 'shipping_state',
				'_shipping_postcode'   => 'shipping_postcode',
				'_shipping_country'    => 'shipping_country',
				'_shipping_phone'      => 'shipping_phone',
			),
		);

		foreach ( $address_props as $props_key => $props ) {
			$props_to_update = $this->get_props_to_update( $order, $props );
			foreach ( $props_to_update as $meta_key => $prop ) {
				$value   = $order->{"get_$prop"}( 'edit' );
				$value   = is_string( $value ) ? wp_slash( $value ) : $value;
				$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

				if ( $updated ) {
					$updated_props[] = $prop;
					$updated_props[] = $props_key;
				}
			}
		}

		parent::update_post_meta( $order );

		// If address changed, store concatenated version to make searches faster.
		if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) {
			update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) );
		}
		if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) {
			update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) );
		}

		// Legacy date handling. @todo remove in 4.0.
		if ( in_array( 'date_paid', $updated_props, true ) ) {
			$value = $order->get_date_paid( 'edit' );
			// In 2.6.x date_paid was stored as _paid_date in local mysql format.
			update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' );
		}

		if ( in_array( 'date_completed', $updated_props, true ) ) {
			$value = $order->get_date_completed( 'edit' );
			// In 2.6.x date_completed was stored as _completed_date in local mysql format.
			update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' );
		}

		// If customer changed, update any downloadable permissions.
		if ( in_array( 'customer_id', $updated_props, true ) || in_array( 'billing_email', $updated_props, true ) ) {
			$data_store = WC_Data_Store::load( 'customer-download' );
			$data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() );
		}

		// Mark user account as active.
		if ( in_array( 'customer_id', $updated_props, true ) ) {
			wc_update_user_last_active( $order->get_customer_id() );
		}

		do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
	}

	/**
	 * Excerpt for post.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	protected function get_post_excerpt( $order ) {
		return $order->get_customer_note();
	}

	/**
	 * Get order key.
	 *
	 * @since 4.3.0
	 * @param WC_order $order Order object.
	 * @return string
	 */
	protected function get_order_key( $order ) {
		if ( '' !== $order->get_order_key() ) {
			return $order->get_order_key();
		}

		return parent::get_order_key( $order );
	}

	/**
	 * Get amount already refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( postmeta.meta_value )
				FROM $wpdb->postmeta AS postmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				WHERE postmeta.meta_key = '_refund_amount'
				AND postmeta.post_id = posts.ID",
				$order->get_id()
			)
		);

		return floatval( $total );
	}

	/**
	 * Get the total tax refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_tax_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
				$order->get_id()
			)
		) ?? 0;

		return abs( $total );
	}

	/**
	 * Get the total shipping refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_shipping_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('cost')",
				$order->get_id()
			)
		) ?? 0;

		return abs( $total );
	}

	/**
	 * Finds an Order ID based on an order key.
	 *
	 * @param string $order_key An order key has generated by.
	 * @return int The ID of an order, or 0 if the order could not be found
	 */
	public function get_order_id_by_order_key( $order_key ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) );
	}

	/**
	 * Return count of orders with a specific status.
	 *
	 * @param  string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses.
	 * @return int
	 */
	public function get_order_count( $status ) {
		global $wpdb;
		return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) );
	}

	/**
	 * Get all orders matching the passed in args.
	 *
	 * @deprecated 3.1.0 - Use wc_get_orders instead.
	 * @see    wc_get_orders()
	 *
	 * @param  array $args List of args passed to wc_get_orders().
	 *
	 * @return array|object
	 */
	public function get_orders( $args = array() ) {
		wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' );
		return wc_get_orders( $args );
	}

	/**
	 * Generate meta query for wc_get_orders.
	 *
	 * @param  array  $values List of customers ids or emails.
	 * @param  string $relation 'or' or 'and' relation used to build the WP meta_query.
	 * @return array
	 */
	private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) {
		$meta_query = array(
			'relation'        => strtoupper( $relation ),
			'customer_emails' => array(
				'key'     => '_billing_email',
				'value'   => array(),
				'compare' => 'IN',
			),
			'customer_ids'    => array(
				'key'     => '_customer_user',
				'value'   => array(),
				'compare' => 'IN',
			),
		);
		foreach ( $values as $value ) {
			if ( is_array( $value ) ) {
				$query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' );
				if ( is_wp_error( $query_part ) ) {
					return $query_part;
				}
				$meta_query[] = $query_part;
			} elseif ( is_email( $value ) ) {
				$meta_query['customer_emails']['value'][] = sanitize_email( $value );
			} elseif ( is_numeric( $value ) ) {
				$meta_query['customer_ids']['value'][] = strval( absint( $value ) );
			} else {
				return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values );
			}
		}

		if ( empty( $meta_query['customer_emails']['value'] ) ) {
			unset( $meta_query['customer_emails'] );
			unset( $meta_query['relation'] );
		}

		if ( empty( $meta_query['customer_ids']['value'] ) ) {
			unset( $meta_query['customer_ids'] );
			unset( $meta_query['relation'] );
		}

		return $meta_query;
	}

	/**
	 * Get unpaid orders after a certain date,
	 *
	 * @param  int $date Timestamp.
	 * @return array
	 */
	public function get_unpaid_orders( $date ) {
		global $wpdb;

		$unpaid_orders = $wpdb->get_col(
			$wpdb->prepare(
			// @codingStandardsIgnoreStart
				"SELECT posts.ID
				FROM {$wpdb->posts} AS posts
				WHERE   posts.post_type   IN ('" . implode( "','", wc_get_order_types() ) . "')
				AND     posts.post_status = 'wc-pending'
				AND     posts.post_modified < %s",
				// @codingStandardsIgnoreEnd
				gmdate( 'Y-m-d H:i:s', absint( $date ) )
			)
		);

		return $unpaid_orders;
	}

	/**
	 * Search order data for a term and return ids.
	 *
	 * @param  string $term Searched term.
	 * @return array of ids
	 */
	public function search_orders( $term ) {
		global $wpdb;

		/**
		 * Searches on meta data can be slow - this lets you choose what fields to search.
		 * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster.
		 * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed).
		 *
		 * @var array
		 */
		$search_fields = array_map(
			'wc_clean',
			apply_filters(
				'woocommerce_shop_order_search_fields',
				array(
					'_billing_address_index',
					'_shipping_address_index',
					'_billing_last_name',
					'_billing_email',
					'_billing_phone',
				)
			)
		);
		$order_ids     = array();

		if ( is_numeric( $term ) ) {
			$order_ids[] = absint( $term );
		}

		if ( ! empty( $search_fields ) ) {
			$order_ids = array_unique(
				array_merge(
					$order_ids,
					$wpdb->get_col(
						$wpdb->prepare(
							"SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine
							'%' . $wpdb->esc_like( wc_clean( $term ) ) . '%'
						)
					),
					$wpdb->get_col(
						$wpdb->prepare(
							"SELECT order_id
							FROM {$wpdb->prefix}woocommerce_order_items as order_items
							WHERE order_item_name LIKE %s",
							'%' . $wpdb->esc_like( wc_clean( $term ) ) . '%'
						)
					),
					$wpdb->get_col(
						$wpdb->prepare(
							"SELECT DISTINCT os.order_id FROM {$wpdb->prefix}wc_order_stats os
							INNER JOIN {$wpdb->prefix}wc_customer_lookup cl ON os.customer_id = cl.customer_id
							INNER JOIN {$wpdb->usermeta} um ON cl.user_id = um.user_id
							WHERE (um.meta_key = 'billing_phone' OR um.meta_key = 'shipping_phone')
							AND um.meta_value = %s",
							wc_clean( $term )
						)
					)
				)
			);
		}

		return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields );
	}

	/**
	 * Gets information about whether permissions were generated yet.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_download_permissions_granted( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) );
	}

	/**
	 * Stores information about whether permissions were generated yet.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_download_permissions_granted( $order, $set ) {
		if ( $order instanceof WC_Order ) {
			$order->set_download_permissions_granted( $set );
		}
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) );
	}

	/**
	 * Gets information about whether sales were recorded.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_recorded_sales( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) );
	}

	/**
	 * Stores information about whether sales were recorded.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_recorded_sales( $order, $set ) {
		if ( $order instanceof WC_Order ) {
			$order->set_recorded_sales( $set );
		}
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) );
	}

	/**
	 * Gets information about whether coupon counts were updated.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_recorded_coupon_usage_counts( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) );
	}

	/**
	 * Stores information about whether coupon counts were updated.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_recorded_coupon_usage_counts( $order, $set ) {
		if ( $order instanceof WC_Order ) {
			$order->set_recorded_coupon_usage_counts( $set );
		}
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) );
	}

	/**
	 * Whether email have been sent for this order.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 *
	 * @return bool               Whether email is sent.
	 */
	public function get_email_sent( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_new_order_email_sent', true ) );
	}

	/**
	 * Whether email have been sent for this order.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 *
	 * @return bool               Whether email is sent.
	 */
	public function get_new_order_email_sent( $order ) {
		return $this->get_email_sent( $order );
	}

	/**
	 * Stores information about whether email was sent.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_email_sent( $order, $set ) {
		if ( $order instanceof WC_Order ) {
			$order->set_new_order_email_sent( $set );
		}
		$order_id = WC_Order_Factory::get_order_id( $order );
		$value    = wc_bool_to_string( $set );
		$value    = 'yes' === $value ? 'true' : 'false'; // For backward compat, we store this as true|false string.
		update_post_meta( $order_id, '_new_order_email_sent', $value );
	}

	/**
	 * Stores information about whether email was sent.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_new_order_email_sent( $order, $set ) {
		$this->set_email_sent( $order, $set );
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys.
	 * Pass $coupon_id if key for only one of the coupon is needed.
	 *
	 * @param WC_Order $order     Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon.
	 */
	public function get_coupon_held_keys( $order, $coupon_id = null ) {
		$held_keys = $order->get_meta( '_coupon_held_keys' );
		if ( $coupon_id ) {
			return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null;
		}
		return $held_keys;
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys.
	 *
	 * @param WC_Order $order Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return mixed
	 */
	public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) {
		$held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' );
		if ( $coupon_id ) {
			return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null;
		}
		return $held_keys_for_user;
	}

	/**
	 * Add/Update list of meta keys that are currently being used by this order to hold a coupon.
	 * This is used to figure out what all meta entries we should delete when order is cancelled/completed.
	 *
	 * @param WC_Order $order              Order object.
	 * @param array    $held_keys          Array of coupon_code => meta_key.
	 * @param array    $held_keys_for_user Array of coupon_code => meta_key for held coupon for user.
	 *
	 * @return mixed
	 */
	public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) {
		if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) {
			$order->update_meta_data( '_coupon_held_keys', $held_keys );
		}
		if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) {
			$order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user );
		}
	}

	/**
	 * Release all coupons held by this order.
	 *
	 * @param WC_Order $order Current order object.
	 * @param bool     $save  Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request.
	 */
	public function release_held_coupons( $order, $save = true ) {
		$coupon_held_keys = $this->get_coupon_held_keys( $order );
		if ( is_array( $coupon_held_keys ) ) {
			foreach ( $coupon_held_keys as $coupon_id => $meta_key ) {
				delete_post_meta( $coupon_id, $meta_key );
			}
		}
		$order->delete_meta_data( '_coupon_held_keys' );

		$coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order );
		if ( is_array( $coupon_held_keys_for_users ) ) {
			foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) {
				delete_post_meta( $coupon_id, $meta_key );
			}
		}
		$order->delete_meta_data( '_coupon_held_keys_for_users' );

		if ( $save ) {
			$order->save_meta_data();
		}

	}

	/**
	 * Gets information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_stock_reduced( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) );
	}

	/**
	 * Stores information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_order_stock_reduced( $order ) {
		return $this->get_stock_reduced( $order );
	}

	/**
	 * Stores information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_stock_reduced( $order, $set ) {
		if ( $order instanceof WC_Order ) {
			$order->set_order_stock_reduced( $set );
		}
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) );
	}

	/**
	 * Gets information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_order_stock_reduced( $order, $set ) {
		$this->set_stock_reduced( $order, $set );
	}

	/**
	 * Get the order type based on Order ID.
	 *
	 * @since 3.0.0
	 * @param int|WP_Post $order Order | Order id.
	 *
	 * @return string
	 */
	public function get_order_type( $order ) {
		return get_post_type( $order );
	}

	/**
	 * Get valid WP_Query args from a WC_Order_Query's query variables.
	 *
	 * @since 3.1.0
	 * @param array $query_vars query vars from a WC_Order_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
		$key_mapping = array(
			'customer_id'    => 'customer_user',
			'status'         => 'post_status',
			'currency'       => 'order_currency',
			'version'        => 'order_version',
			'discount_total' => 'cart_discount',
			'discount_tax'   => 'cart_discount_tax',
			'shipping_total' => 'order_shipping',
			'shipping_tax'   => 'order_shipping_tax',
			'cart_tax'       => 'order_tax',
			'total'          => 'order_total',
			'page'           => 'paged',
		);

		foreach ( $key_mapping as $query_key => $db_key ) {
			if ( isset( $query_vars[ $query_key ] ) ) {
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
				unset( $query_vars[ $query_key ] );
			}
		}

		// Add the 'wc-' prefix to status if needed.
		if ( ! empty( $query_vars['post_status'] ) ) {
			if ( is_array( $query_vars['post_status'] ) ) {
				foreach ( $query_vars['post_status'] as &$status ) {
					$status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status;
				}
			} else {
				$query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status'];
			}
		}

		$wp_query_args = parent::get_wp_query_args( $query_vars );

		if ( ! isset( $wp_query_args['date_query'] ) ) {
			$wp_query_args['date_query'] = array();
		}
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			$wp_query_args['meta_query'] = array();
		}

		$date_queries = array(
			'date_created'   => 'post_date',
			'date_modified'  => 'post_modified',
			'date_completed' => '_date_completed',
			'date_paid'      => '_date_paid',
		);
		foreach ( $date_queries as $query_var_key => $db_key ) {
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {

				// Remove any existing meta queries for the same keys to prevent conflicts.
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
				$meta_query_index = array_search( $db_key, $existing_queries, true );
				if ( false !== $meta_query_index ) {
					unset( $wp_query_args['meta_query'][ $meta_query_index ] );
				}

				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
			}
		}

		if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) {
			$values         = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] );
			$customer_query = $this->get_orders_generate_customer_meta_query( $values );
			if ( is_wp_error( $customer_query ) ) {
				$wp_query_args['errors'][] = $customer_query;
			} else {
				$wp_query_args['meta_query'][] = $customer_query;
			}
		}

		if ( isset( $query_vars['anonymized'] ) ) {
			if ( $query_vars['anonymized'] ) {
				$wp_query_args['meta_query'][] = array(
					'key'   => '_anonymized',
					'value' => 'yes',
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => '_anonymized',
					'compare' => 'NOT EXISTS',
				);
			}
		}

		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$wp_query_args['no_found_rows'] = true;
		}

		return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this );
	}

	/**
	 * Query for Orders matching specific criteria.
	 *
	 * @since 3.1.0
	 *
	 * @param array $query_vars query vars from a WC_Order_Query.
	 *
	 * @return array|object
	 */
	public function query( $query_vars ) {
		$args = $this->get_wp_query_args( $query_vars );

		if ( ! empty( $args['errors'] ) ) {
			$query = (object) array(
				'posts'         => array(),
				'found_posts'   => 0,
				'max_num_pages' => 0,
			);
		} else {
			$query = new WP_Query( $args );
		}

		if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
			$orders = $query->posts;
		} else {
			update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches.
			$order_ids = wp_list_pluck( $query->posts, 'ID' );
			$orders    = $this->compile_orders( $order_ids, $query_vars, $query );
		}

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'orders'        => $orders,
				'total'         => $query->found_posts,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $orders;
	}

	/**
	 * Compile order response and set caches as needed for order ids.
	 *
	 * @param array    $order_ids  List of order IDS to compile.
	 * @param array    $query_vars Original query arguments.
	 * @param WP_Query $query      Query object.
	 *
	 * @return array Orders.
	 */
	private function compile_orders( $order_ids, $query_vars, $query ) {
		if ( empty( $order_ids ) ) {
			return array();
		}
		$orders = array();

		$this->prime_caches_for_orders( $order_ids, $query_vars );

		foreach ( $query->posts as $post ) {
			$order = wc_get_order( $post );

			// If the order returns false, don't add it to the list.
			if ( false === $order ) {
				continue;
			}

			$orders[] = $order;
		}

		return $orders;
	}

	/**
	 * Helper method to prime caches for orders. Call this if you are going to be fetching orders in a loop.
	 *
	 * @param array $order_ids List of order IDS to prime caches for.
	 * @param array $query_vars Original query arguments.
	 */
	public function prime_caches_for_orders( $order_ids, $query_vars ) {
		// Lets do some cache hydrations so that we don't have to fetch data from DB for every order.
		$this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars );
		$this->prime_refund_caches_for_order( $order_ids, $query_vars );
		$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
	}

	/**
	 * Prime refund cache for orders.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	private function prime_refund_caches_for_order( $order_ids, $query_vars ) {
		if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) {
			return;
		}
		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'], true ) ) {
				return;
			}
		}
		$cache_keys_mapping = array();
		foreach ( $order_ids as $order_id ) {
			$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
		}
		$non_cached_ids = array();
		$cache_values   = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}

		$refunds       = wc_get_orders(
			array(
				'type'            => 'shop_order_refund',
				'post_parent__in' => $non_cached_ids,
				'limit'           => - 1,
			)
		);
		$order_refunds = array_reduce(
			$refunds,
			function ( $order_refunds_array, WC_Order_Refund $refund ) {
				if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) {
					$order_refunds_array[ $refund->get_parent_id() ] = array();
				}
				$order_refunds_array[ $refund->get_parent_id() ][] = $refund;
				return $order_refunds_array;
			},
			array()
		);
		foreach ( $non_cached_ids as $order_id ) {
			$refunds = array();
			if ( isset( $order_refunds[ $order_id ] ) ) {
				$refunds = $order_refunds[ $order_id ];
			}
			wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' );
		}
	}

	/**
	 * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) {
		global $wpdb;

		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'], true ) ) {
				return;
			}
		}

		$cache_keys_mapping = array();
		foreach ( $order_ids as $order_id ) {
			$cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' );
		}
		$cache_values   = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
		$non_cached_ids = array();
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}
		$order_ids           = esc_sql( $non_cached_ids );
		$order_ids_in        = "'" . implode( "', '", $order_ids ) . "'";
		$raw_meta_data_array = $wpdb->get_results(
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT post_id as object_id, meta_id, meta_key, meta_value
				FROM {$wpdb->postmeta}
				WHERE post_id IN ( $order_ids_in )
				ORDER BY post_id"
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
		$raw_meta_data_collection = array_reduce(
			$raw_meta_data_array,
			function ( $collection, $raw_meta_data ) {
				if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) {
					$collection[ $raw_meta_data->object_id ] = array();
				}
				$collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
				return $collection;
			},
			array()
		);
		WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
	}

	/**
	 * Attempts to restore the specified order back to its original status (after having been trashed).
	 *
	 * @param WC_Order $order The order to be untrashed.
	 *
	 * @return bool If the operation was successful.
	 */
	public function untrash_order( WC_Order $order ): bool {
		if ( ! wp_untrash_post( $order->get_id() ) ) {
			return false;
		}

		$order->set_status( get_post_field( 'post_status', $order->get_id() ) );
		return (bool) $order->save();
	}
}
class-wc-order-item-coupon-data-store.php000064400000003044151542600450014403 0ustar00<?php
/**
 * Class WC_Order_Item_Coupon_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Coupon Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Coupon_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'discount_amount', 'discount_amount_tax' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Coupon $item Coupon order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'discount'     => get_metadata( 'order_item', $id, 'discount_amount', true ),
				'discount_tax' => get_metadata( 'order_item', $id, 'discount_amount_tax', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $item->get_id() will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Coupon $item Coupon order item.
	 */
	public function save_item_data( &$item ) {
		$id          = $item->get_id();
		$save_values = array(
			'discount_amount'     => $item->get_discount( 'edit' ),
			'discount_amount_tax' => $item->get_discount_tax( 'edit' ),
		);
		foreach ( $save_values as $key => $value ) {
			update_metadata( 'order_item', $id, $key, $value );
		}
	}
}
class-wc-order-item-data-store.php000064400000011673151542600450013111 0ustar00<?php
/**
 * Class WC_Order_Item_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Data Store: Misc Order Item Data functions.
 *
 * @version  3.0.0
 */
class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface {

	/**
	 * Add an order item to an order.
	 *
	 * @since  3.0.0
	 * @param  int   $order_id Order ID.
	 * @param  array $item order_item_name and order_item_type.
	 * @return int Order Item ID
	 */
	public function add_order_item( $order_id, $item ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_order_items',
			array(
				'order_item_name' => $item['order_item_name'],
				'order_item_type' => $item['order_item_type'],
				'order_id'        => $order_id,
			),
			array(
				'%s',
				'%s',
				'%d',
			)
		);

		$item_id = absint( $wpdb->insert_id );

		$this->clear_caches( $item_id, $order_id );

		return $item_id;
	}

	/**
	 * Update an order item.
	 *
	 * @since  3.0.0
	 * @param  int   $item_id Item ID.
	 * @param  array $item order_item_name or order_item_type.
	 * @return boolean
	 */
	public function update_order_item( $item_id, $item ) {
		global $wpdb;
		$updated = $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) );
		$this->clear_caches( $item_id, null );
		return $updated;
	}

	/**
	 * Delete an order item.
	 *
	 * @since  3.0.0
	 * @param  int $item_id Item ID.
	 */
	public function delete_order_item( $item_id ) {
		// Load the order ID before the deletion, since after, it won't exist in the database.
		$order_id = $this->get_order_id_by_order_item_id( $item_id );

		global $wpdb;
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) );

		$this->clear_caches( $item_id, $order_id );
	}

	/**
	 * Update term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  string $prev_value (default: '').
	 * @return bool
	 */
	public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
		return update_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value );
	}

	/**
	 * Add term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  bool   $unique (default: false).
	 * @return int    New row ID or 0
	 */
	public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ) {
		return add_metadata( 'order_item', $item_id, wp_slash( $meta_key ), is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $unique );
	}

	/**
	 * Delete term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value (default: '').
	 * @param  bool   $delete_all (default: false).
	 * @return bool
	 */
	public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ) {
		return delete_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $delete_all );
	}

	/**
	 * Get term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $key Meta key.
	 * @param  bool   $single (default: true).
	 * @return mixed
	 */
	public function get_metadata( $item_id, $key, $single = true ) {
		return get_metadata( 'order_item', $item_id, $key, $single );
	}

	/**
	 * Get order ID by order item ID.
	 *
	 * @since 3.0.0
	 * @param  int $item_id Item ID.
	 * @return int
	 */
	public function get_order_id_by_order_item_id( $item_id ) {
		global $wpdb;
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d",
				$item_id
			)
		);
	}

	/**
	 * Get the order item type based on Item ID.
	 *
	 * @since 3.0.0
	 * @param int $item_id Item ID.
	 * @return string|null Order item type or null if no order item entry found.
	 */
	public function get_order_item_type( $item_id ) {
		global $wpdb;
		$order_item_type = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;",
				$item_id
			)
		);

		return $order_item_type;
	}

	/**
	 * Clear meta cache.
	 *
	 * @param int      $item_id Item ID.
	 * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID.
	 */
	protected function clear_caches( $item_id, $order_id ) {
		wp_cache_delete( 'item-' . $item_id, 'order-items' );

		if ( ! $order_id ) {
			$order_id = $this->get_order_id_by_order_item_id( $item_id );
		}
		if ( $order_id ) {
			wp_cache_delete( 'order-items-' . $order_id, 'orders' );
		}
	}
}
class-wc-order-item-fee-data-store.php000064400000004005151542600450013635 0ustar00<?php
/**
 * Class WC_Order_Item_Fee_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Fee Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Fee_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( '_fee_amount', '_tax_class', '_tax_status', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Fee $item Fee order item object.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'amount'     => get_metadata( 'order_item', $id, '_fee_amount', true ),
				'tax_class'  => get_metadata( 'order_item', $id, '_tax_class', true ),
				'tax_status' => get_metadata( 'order_item', $id, '_tax_status', true ),
				'total'      => get_metadata( 'order_item', $id, '_line_total', true ),
				'taxes'      => get_metadata( 'order_item', $id, '_line_tax_data', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Fee $item Fee order item object.
	 */
	public function save_item_data( &$item ) {
		$id          = $item->get_id();
		$save_values = array(
			'_fee_amount'    => $item->get_amount( 'edit' ),
			'_tax_class'     => $item->get_tax_class( 'edit' ),
			'_tax_status'    => $item->get_tax_status( 'edit' ),
			'_line_total'    => $item->get_total( 'edit' ),
			'_line_tax'      => $item->get_total_tax( 'edit' ),
			'_line_tax_data' => $item->get_taxes( 'edit' ),
		);
		foreach ( $save_values as $key => $value ) {
			update_metadata( 'order_item', $id, $key, $value );
		}
	}
}
class-wc-order-item-product-data-store.php000064400000006210151542600450014556 0ustar00<?php
/**
 * Class WC_Order_Item_Product_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Product Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'product_id'   => get_metadata( 'order_item', $id, '_product_id', true ),
				'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ),
				'quantity'     => get_metadata( 'order_item', $id, '_qty', true ),
				'tax_class'    => get_metadata( 'order_item', $id, '_tax_class', true ),
				'subtotal'     => get_metadata( 'order_item', $id, '_line_subtotal', true ),
				'total'        => get_metadata( 'order_item', $id, '_line_total', true ),
				'taxes'        => get_metadata( 'order_item', $id, '_line_tax_data', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'_product_id'        => 'product_id',
			'_variation_id'      => 'variation_id',
			'_qty'               => 'quantity',
			'_tax_class'         => 'tax_class',
			'_line_subtotal'     => 'subtotal',
			'_line_subtotal_tax' => 'subtotal_tax',
			'_line_total'        => 'total',
			'_line_tax'          => 'total_tax',
			'_line_tax_data'     => 'taxes',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}

	/**
	 * Get a list of download IDs for a specific item from an order.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 * @param WC_Order              $order Order object.
	 * @return array
	 */
	public function get_download_ids( $item, $order ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id",
				$order->get_billing_email(),
				$order->get_order_key(),
				$item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id()
			)
		);
	}
}
class-wc-order-item-shipping-data-store.php000064400000004304151542600450014721 0ustar00<?php
/**
 * WC Order Item Shipping Data Store
 *
 * @version 3.0.0
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Order_Item_Shipping_Data_Store class.
 */
class WC_Order_Item_Shipping_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'method_id', 'instance_id', 'cost', 'total_tax', 'taxes' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Shipping $item Item to read to.
	 * @throws Exception If invalid shipping order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'method_id'   => get_metadata( 'order_item', $id, 'method_id', true ),
				'instance_id' => get_metadata( 'order_item', $id, 'instance_id', true ),
				'total'       => get_metadata( 'order_item', $id, 'cost', true ),
				'taxes'       => get_metadata( 'order_item', $id, 'taxes', true ),
			)
		);

		// BW compat.
		if ( '' === $item->get_instance_id() && strstr( $item->get_method_id(), ':' ) ) {
			$legacy_method_id = explode( ':', $item->get_method_id() );
			$item->set_method_id( $legacy_method_id[0] );
			$item->set_instance_id( $legacy_method_id[1] );
		}

		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Shipping $item Item to save.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'method_id'   => 'method_id',
			'instance_id' => 'instance_id',
			'cost'        => 'total',
			'total_tax'   => 'total_tax',
			'taxes'       => 'taxes',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}
}
class-wc-order-item-tax-data-store.php000064400000004362151542600450013700 0ustar00<?php
/**
 * Class WC_Order_Item_Tax_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Tax Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Tax_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'rate_id', 'label', 'compound', 'tax_amount', 'shipping_tax_amount', 'rate_percent' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Tax $item Tax order item object.
	 * @throws Exception If invalid order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'rate_id'            => get_metadata( 'order_item', $id, 'rate_id', true ),
				'label'              => get_metadata( 'order_item', $id, 'label', true ),
				'compound'           => get_metadata( 'order_item', $id, 'compound', true ),
				'tax_total'          => get_metadata( 'order_item', $id, 'tax_amount', true ),
				'shipping_tax_total' => get_metadata( 'order_item', $id, 'shipping_tax_amount', true ),
				'rate_percent'       => get_metadata( 'order_item', $id, 'rate_percent', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Tax $item Tax order item object.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'rate_id'             => 'rate_id',
			'label'               => 'label',
			'compound'            => 'compound',
			'tax_amount'          => 'tax_total',
			'shipping_tax_amount' => 'shipping_tax_total',
			'rate_percent'        => 'rate_percent',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}
}
class-wc-order-refund-data-store-cpt.php000064400000007064151542600450014221 0ustar00<?php
/**
 * Class WC_Order_Refund_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Refund Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Refund_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_order_currency',
		'_cart_discount',
		'_refund_amount',
		'_refunded_by',
		'_refunded_payment',
		'_refund_reason',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_order_version',
		'_prices_include_tax',
		'_payment_tokens',
	);

	/**
	 * Delete a refund - no trash is supported.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $args Array of args to pass to the delete method.
	 */
	public function delete( &$order, $args = array() ) {
		$id               = $order->get_id();
		$parent_order_id  = $order->get_parent_id();
		$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id;

		if ( ! $id ) {
			return;
		}

		wp_delete_post( $id );
		wp_cache_delete( $refund_cache_key, 'orders' );
		$order->set_id( 0 );
		do_action( 'woocommerce_delete_order_refund', $id );
	}

	/**
	 * Read refund data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order_Refund $refund Refund object.
	 * @param object          $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$refund, $post_object ) {
		parent::read_order_data( $refund, $post_object );
		$id = $refund->get_id();
		$refund->set_props(
			array(
				'amount'           => get_post_meta( $id, '_refund_amount', true ),
				'refunded_by'      => metadata_exists( 'post', $id, '_refunded_by' ) ? get_post_meta( $id, '_refunded_by', true ) : absint( $post_object->post_author ),
				'refunded_payment' => wc_string_to_bool( get_post_meta( $id, '_refunded_payment', true ) ),
				'reason'           => metadata_exists( 'post', $id, '_refund_reason' ) ? get_post_meta( $id, '_refund_reason', true ) : $post_object->post_excerpt,
			)
		);
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order_Refund $refund Refund object.
	 *
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$refund ) {
		parent::update_post_meta( $refund );

		$updated_props     = array();
		$meta_key_to_props = array(
			'_refund_amount'    => 'amount',
			'_refunded_by'      => 'refunded_by',
			'_refunded_payment' => 'refunded_payment',
			'_refund_reason'    => 'reason',
		);

		$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $refund->{"get_$prop"}( 'edit' );
			update_post_meta( $refund->get_id(), $meta_key, $value );
			$updated_props[] = $prop;
		}

		do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props );
	}

	/**
	 * Get a title for the new post type.
	 *
	 * @return string
	 */
	protected function get_post_title() {
		return sprintf(
			/* translators: %s: Order date */
			__( 'Refund &ndash; %s', 'woocommerce' ),
			( new DateTime( 'now' ) )->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText
		);
	}
}
class-wc-payment-token-data-store.php000064400000024657151542600450013643 0ustar00<?php
/**
 * Class WC_Payment_Token_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Payment Token Data Store: Custom Table.
 *
 * @version  3.0.0
 */
class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Payment_Token_Data_Store_Interface {

	/**
	 * Meta type. Payment tokens are a new object type.
	 *
	 * @var string
	 */
	protected $meta_type = 'payment_token';

	/**
	 * If we have already saved our extra data, don't do automatic / default handling.
	 *
	 * @var bool
	 */
	protected $extra_data_saved = false;

	/**
	 * Create a new payment token in the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid or missing payment token fields.
	 */
	public function create( &$token ) {
		if ( false === $token->validate() ) {
			throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
		}

		global $wpdb;
		if ( ! $token->is_default() && $token->get_user_id() > 0 ) {
			$default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() );
			if ( is_null( $default_token ) ) {
				$token->set_default( true );
			}
		}

		$payment_token_data = array(
			'gateway_id' => $token->get_gateway_id( 'edit' ),
			'token'      => $token->get_token( 'edit' ),
			'user_id'    => $token->get_user_id( 'edit' ),
			'type'       => $token->get_type( 'edit' ),
		);

		$wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data );
		$token_id = $wpdb->insert_id;
		$token->set_id( $token_id );
		$this->save_extra_data( $token, true );
		$token->save_meta_data();
		$token->apply_changes();

		// Make sure all other tokens are not set to default.
		if ( $token->is_default() && $token->get_user_id() > 0 ) {
			WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id );
		}

		do_action( 'woocommerce_new_payment_token', $token_id, $token );
	}

	/**
	 * Update a payment token.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid or missing payment token fields.
	 */
	public function update( &$token ) {
		if ( false === $token->validate() ) {
			throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
		}

		global $wpdb;

		$updated_props = array();
		$core_props    = array( 'gateway_id', 'token', 'user_id', 'type' );
		$changed_props = array_keys( $token->get_changes() );

		foreach ( $changed_props as $prop ) {
			if ( ! in_array( $prop, $core_props, true ) ) {
				continue;
			}
			$updated_props[]             = $prop;
			$payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' );
		}

		if ( ! empty( $payment_token_data ) ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_payment_tokens',
				$payment_token_data,
				array( 'token_id' => $token->get_id() )
			);
		}

		$updated_extra_props = $this->save_extra_data( $token );
		$updated_props       = array_merge( $updated_props, $updated_extra_props );
		$token->save_meta_data();
		$token->apply_changes();

		// Make sure all other tokens are not set to default.
		if ( $token->is_default() && $token->get_user_id() > 0 ) {
			WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() );
		}

		do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props );
		do_action( 'woocommerce_payment_token_updated', $token->get_id() );
	}

	/**
	 * Remove a payment token from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Payment_Token $token Payment token object.
	 * @param bool             $force_delete Unused param.
	 */
	public function delete( &$token, $force_delete = false ) {
		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) );
		$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) );
		do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token );
	}

	/**
	 * Read a token from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid payment token.
	 */
	public function read( &$token ) {
		global $wpdb;

		$data = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1",
				$token->get_id()
			)
		);

		if ( $data ) {
			$token->set_props(
				array(
					'token'      => $data->token,
					'user_id'    => $data->user_id,
					'gateway_id' => $data->gateway_id,
					'default'    => $data->is_default,
				)
			);
			$this->read_extra_data( $token );
			$token->read_meta_data();
			$token->set_object_read( true );
			do_action( 'woocommerce_payment_token_loaded', $token );
		} else {
			throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) );
		}
	}

	/**
	 * Read extra data associated with the token (like last4 digits of a card for expiry dates).
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 * @since 3.0.0
	 */
	protected function read_extra_data( &$token ) {
		foreach ( $token->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $token, $function ) ) ) {
				$token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) );
			}
		}
	}

	/**
	 * Saves extra token data as meta.
	 *
	 * @since 3.0.0
	 * @param WC_Payment_Token $token Payment token object.
	 * @param bool             $force By default, only changed props are updated. When this param is true all props are updated.
	 * @return array List of updated props.
	 */
	protected function save_extra_data( &$token, $force = false ) {
		if ( $this->extra_data_saved ) {
			return array();
		}

		$updated_props     = array();
		$extra_data_keys   = $token->get_extra_data_keys();
		$meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array();
		$props_to_update   = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props );

		foreach ( $extra_data_keys as $key ) {
			if ( ! array_key_exists( $key, $props_to_update ) ) {
				continue;
			}
			$function = 'get_' . $key;
			if ( is_callable( array( $token, $function ) ) ) {
				if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) {
					$updated_props[] = $key;
				}
			}
		}

		return $updated_props;
	}

	/**
	 * Returns an array of objects (stdObject) matching specific token criteria.
	 * Accepts token_id, user_id, gateway_id, and type.
	 * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param array $args List of accepted args: token_id, gateway_id, user_id, type.
	 * @return array
	 */
	public function get_tokens( $args ) {
		global $wpdb;
		$args = wp_parse_args(
			$args,
			array(
				'token_id'   => '',
				'user_id'    => '',
				'gateway_id' => '',
				'type'       => '',
			)
		);

		$sql   = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens";
		$where = array( '1=1' );

		if ( $args['token_id'] ) {
			$token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) );
			$where[]   = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')";
		}

		if ( $args['user_id'] ) {
			$where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) );
		}

		if ( $args['gateway_id'] ) {
			$gateway_ids = array( $args['gateway_id'] );
		} else {
			$gateways    = WC_Payment_Gateways::instance();
			$gateway_ids = $gateways->get_payment_gateway_ids();
		}

		$page           = isset( $args['page'] ) ? absint( $args['page'] ) : 1;
		$posts_per_page = absint( isset( $args['limit'] ) ? $args['limit'] : get_option( 'posts_per_page' ) );

		$pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', ';
		$limits = 'LIMIT ' . $pgstrt . $posts_per_page;

		$gateway_ids[] = '';
		$where[]       = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')";

		if ( $args['type'] ) {
			$where[] = $wpdb->prepare( 'type = %s', $args['type'] );
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits );

		return $token_results;
	}

	/**
	 * Returns an stdObject of a token for a user's default token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param int $user_id User ID.
	 * @return object
	 */
	public function get_users_default_token( $user_id ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1",
				$user_id
			)
		);
	}

	/**
	 * Returns an stdObject of a token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return object
	 */
	public function get_token_by_id( $token_id ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
				$token_id
			)
		);
	}

	/**
	 * Returns metadata for a specific payment token.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return array
	 */
	public function get_metadata( $token_id ) {
		return get_metadata( 'payment_token', $token_id );
	}

	/**
	 * Get a token's type by ID.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return string
	 */
	public function get_token_type_by_id( $token_id ) {
		global $wpdb;
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
				$token_id
			)
		);
	}

	/**
	 * Update's a tokens default status in the database. Used for quickly
	 * looping through tokens and setting their statuses instead of creating a bunch
	 * of objects.
	 *
	 * @since 3.0.0
	 *
	 * @param int  $token_id Token ID.
	 * @param bool $status Whether given payment token is the default payment token or not.
	 *
	 * @return void
	 */
	public function set_default_status( $token_id, $status = true ) {
		global $wpdb;
		$wpdb->update(
			$wpdb->prefix . 'woocommerce_payment_tokens',
			array( 'is_default' => (int) $status ),
			array(
				'token_id' => $token_id,
			)
		);
	}

}
class-wc-product-data-store-cpt.php000064400000213446151542600450013310 0ustar00<?php
/**
 * WC_Product_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta".
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_visibility',
		'_sku',
		'_price',
		'_regular_price',
		'_sale_price',
		'_sale_price_dates_from',
		'_sale_price_dates_to',
		'total_sales',
		'_tax_status',
		'_tax_class',
		'_manage_stock',
		'_stock',
		'_stock_status',
		'_backorders',
		'_low_stock_amount',
		'_sold_individually',
		'_weight',
		'_length',
		'_width',
		'_height',
		'_upsell_ids',
		'_crosssell_ids',
		'_purchase_note',
		'_default_attributes',
		'_product_attributes',
		'_virtual',
		'_downloadable',
		'_download_limit',
		'_download_expiry',
		'_featured',
		'_downloadable_files',
		'_wc_rating_count',
		'_wc_average_rating',
		'_wc_review_count',
		'_variation_description',
		'_thumbnail_id',
		'_file_paths',
		'_product_image_gallery',
		'_product_version',
		'_wp_old_slug',
		'_edit_last',
		'_edit_lock',
	);

	/**
	 * Meta data which should exist in the DB, even if empty.
	 *
	 * @since 3.6.0
	 *
	 * @var array
	 */
	protected $must_exist_meta_keys = array(
		'_tax_class',
	);

	/**
	 * If we have already saved our extra data, don't do automatic / default handling.
	 *
	 * @var bool
	 */
	protected $extra_data_saved = false;

	/**
	 * Stores updated props.
	 *
	 * @var array
	 */
	protected $updated_props = array();

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Method to create a new product in the database.
	 *
	 * @param WC_Product $product Product object.
	 */
	public function create( &$product ) {
		if ( ! $product->get_date_created( 'edit' ) ) {
			$product->set_date_created( time() );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_product_data',
				array(
					'post_type'      => 'product',
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
					'post_author'    => get_current_user_id(),
					'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
					'post_content'   => $product->get_description(),
					'post_excerpt'   => $product->get_short_description(),
					'post_parent'    => $product->get_parent_id(),
					'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
					'ping_status'    => 'closed',
					'menu_order'     => $product->get_menu_order(),
					'post_password'  => $product->get_post_password( 'edit' ),
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
					'post_name'      => $product->get_slug( 'edit' ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$product->set_id( $id );

			$this->update_post_meta( $product, true );
			$this->update_terms( $product, true );
			$this->update_visibility( $product, true );
			$this->update_attributes( $product, true );
			$this->update_version_and_type( $product );
			$this->handle_updated_props( $product );
			$this->clear_caches( $product );

			$product->save_meta_data();
			$product->apply_changes();

			do_action( 'woocommerce_new_product', $id, $product );
		}
	}

	/**
	 * Method to read a product from the database.
	 *
	 * @param WC_Product $product Product object.
	 * @throws Exception If invalid product.
	 */
	public function read( &$product ) {
		$product->set_defaults();
		$post_object = get_post( $product->get_id() );

		if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) {
			throw new Exception( __( 'Invalid product.', 'woocommerce' ) );
		}

		$product->set_props(
			array(
				'name'              => $post_object->post_title,
				'slug'              => $post_object->post_name,
				'date_created'      => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'     => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'            => $post_object->post_status,
				'description'       => $post_object->post_content,
				'short_description' => $post_object->post_excerpt,
				'parent_id'         => $post_object->post_parent,
				'menu_order'        => $post_object->menu_order,
				'post_password'     => $post_object->post_password,
				'reviews_allowed'   => 'open' === $post_object->comment_status,
			)
		);

		$this->read_attributes( $product );
		$this->read_downloads( $product );
		$this->read_visibility( $product );
		$this->read_product_data( $product );
		$this->read_extra_data( $product );
		$product->set_object_read( true );

		do_action( 'woocommerce_product_read', $product->get_id() );
	}

	/**
	 * Method to update a product in the database.
	 *
	 * @param WC_Product $product Product object.
	 */
	public function update( &$product ) {
		$product->save_meta_data();
		$changes = $product->get_changes();

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug', 'post_password' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_content'   => $product->get_description( 'edit' ),
				'post_excerpt'   => $product->get_short_description( 'edit' ),
				'post_title'     => $product->get_name( 'edit' ),
				'post_parent'    => $product->get_parent_id( 'edit' ),
				'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
				'post_status'    => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
				'menu_order'     => $product->get_menu_order( 'edit' ),
				'post_password'  => $product->get_post_password( 'edit' ),
				'post_name'      => $product->get_slug( 'edit' ),
				'post_type'      => 'product',
			);
			if ( $product->get_date_created( 'edit' ) ) {
				$post_data['post_date']     = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
				$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
			}
			if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
				$post_data['post_modified']     = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
				$post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() );
			} else {
				$post_data['post_modified']     = current_time( 'mysql' );
				$post_data['post_modified_gmt'] = current_time( 'mysql', 1 );
			}

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
				clean_post_cache( $product->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
			}
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

		} else { // Only update post modified time to record this save event.
			$GLOBALS['wpdb']->update(
				$GLOBALS['wpdb']->posts,
				array(
					'post_modified'     => current_time( 'mysql' ),
					'post_modified_gmt' => current_time( 'mysql', 1 ),
				),
				array(
					'ID' => $product->get_id(),
				)
			);
			clean_post_cache( $product->get_id() );
		}

		$this->update_post_meta( $product );
		$this->update_terms( $product );
		$this->update_visibility( $product );
		$this->update_attributes( $product );
		$this->update_version_and_type( $product );
		$this->handle_updated_props( $product );
		$this->clear_caches( $product );

		wc_get_container()
			->get( DownloadPermissionsAdjuster::class )
			->maybe_schedule_adjust_download_permissions( $product );

		$product->apply_changes();

		do_action( 'woocommerce_update_product', $product->get_id(), $product );
	}

	/**
	 * Method to delete a product from the database.
	 *
	 * @param WC_Product $product Product object.
	 * @param array      $args Array of args to pass to the delete method.
	 */
	public function delete( &$product, $args = array() ) {
		$id        = $product->get_id();
		$post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product';

		$args = wp_parse_args(
			$args,
			array(
				'force_delete' => false,
			)
		);

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			do_action( 'woocommerce_before_delete_' . $post_type, $id );
			wp_delete_post( $id );
			$product->set_id( 0 );
			do_action( 'woocommerce_delete_' . $post_type, $id );
		} else {
			wp_trash_post( $id );
			$product->set_status( 'trash' );
			do_action( 'woocommerce_trash_' . $post_type, $id );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Read product data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_product_data( &$product ) {
		$id                = $product->get_id();
		$post_meta_values  = get_post_meta( $id );
		$meta_key_to_props = array(
			'_sku'                   => 'sku',
			'_regular_price'         => 'regular_price',
			'_sale_price'            => 'sale_price',
			'_price'                 => 'price',
			'_sale_price_dates_from' => 'date_on_sale_from',
			'_sale_price_dates_to'   => 'date_on_sale_to',
			'total_sales'            => 'total_sales',
			'_tax_status'            => 'tax_status',
			'_tax_class'             => 'tax_class',
			'_manage_stock'          => 'manage_stock',
			'_backorders'            => 'backorders',
			'_low_stock_amount'      => 'low_stock_amount',
			'_sold_individually'     => 'sold_individually',
			'_weight'                => 'weight',
			'_length'                => 'length',
			'_width'                 => 'width',
			'_height'                => 'height',
			'_upsell_ids'            => 'upsell_ids',
			'_crosssell_ids'         => 'cross_sell_ids',
			'_purchase_note'         => 'purchase_note',
			'_default_attributes'    => 'default_attributes',
			'_virtual'               => 'virtual',
			'_downloadable'          => 'downloadable',
			'_download_limit'        => 'download_limit',
			'_download_expiry'       => 'download_expiry',
			'_thumbnail_id'          => 'image_id',
			'_stock'                 => 'stock_quantity',
			'_stock_status'          => 'stock_status',
			'_wc_average_rating'     => 'average_rating',
			'_wc_rating_count'       => 'rating_counts',
			'_wc_review_count'       => 'review_count',
			'_product_image_gallery' => 'gallery_image_ids',
		);

		$set_props = array();

		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			$meta_value         = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null;
			$set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
		}

		$set_props['category_ids']      = $this->get_term_ids( $product, 'product_cat' );
		$set_props['tag_ids']           = $this->get_term_ids( $product, 'product_tag' );
		$set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) );
		$set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ?? '' ) );

		$product->set_props( $set_props );
	}

	/**
	 * Re-reads stock from the DB ignoring changes.
	 *
	 * @param WC_Product $product Product object.
	 * @param int|float  $new_stock New stock level if already read.
	 */
	public function read_stock_quantity( &$product, $new_stock = null ) {
		$object_read = $product->get_object_read();
		$product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db).
		$product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock );
		$product->set_object_read( $object_read );
	}

	/**
	 * Read extra data associated with the product, like button text or product URL for external products.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_extra_data( &$product ) {
		foreach ( $product->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $product, $function ) ) ) {
				$product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
			}
		}
	}

	/**
	 * Convert visibility terms to props.
	 * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_visibility( &$product ) {
		$terms           = get_the_terms( $product->get_id(), 'product_visibility' );
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
		$featured        = in_array( 'featured', $term_names, true );
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );

		if ( $exclude_search && $exclude_catalog ) {
			$catalog_visibility = 'hidden';
		} elseif ( $exclude_search ) {
			$catalog_visibility = 'catalog';
		} elseif ( $exclude_catalog ) {
			$catalog_visibility = 'search';
		} else {
			$catalog_visibility = 'visible';
		}

		$product->set_props(
			array(
				'featured'           => $featured,
				'catalog_visibility' => $catalog_visibility,
			)
		);
	}

	/**
	 * Read attributes from post meta.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function read_attributes( &$product ) {
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );

		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
			$attributes = array();
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
				$meta_value = array_merge(
					array(
						'name'         => '',
						'value'        => '',
						'position'     => 0,
						'is_visible'   => 0,
						'is_variation' => 0,
						'is_taxonomy'  => 0,
					),
					(array) $meta_attribute_value
				);

				// Check if is a taxonomy attribute.
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
						continue;
					}
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
				} else {
					$id      = 0;
					$options = wc_get_text_attributes( $meta_value['value'] );
				}

				$attribute = new WC_Product_Attribute();
				$attribute->set_id( $id );
				$attribute->set_name( $meta_value['name'] );
				$attribute->set_options( $options );
				$attribute->set_position( $meta_value['position'] );
				$attribute->set_visible( $meta_value['is_visible'] );
				$attribute->set_variation( $meta_value['is_variation'] );
				$attributes[] = $attribute;
			}
			$product->set_attributes( $attributes );
		}
	}

	/**
	 * Read downloads from post meta.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_downloads( &$product ) {
		$meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );

		if ( $meta_values ) {
			$downloads = array();
			foreach ( $meta_values as $key => $value ) {
				if ( ! isset( $value['name'], $value['file'] ) ) {
					continue;
				}
				$download = new WC_Product_Download();
				$download->set_id( $key );
				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
				$downloads[] = $download;
			}
			$product->set_downloads( $downloads );
		}
	}

	/**
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_sku'                   => 'sku',
			'_regular_price'         => 'regular_price',
			'_sale_price'            => 'sale_price',
			'_sale_price_dates_from' => 'date_on_sale_from',
			'_sale_price_dates_to'   => 'date_on_sale_to',
			'total_sales'            => 'total_sales',
			'_tax_status'            => 'tax_status',
			'_tax_class'             => 'tax_class',
			'_manage_stock'          => 'manage_stock',
			'_backorders'            => 'backorders',
			'_low_stock_amount'      => 'low_stock_amount',
			'_sold_individually'     => 'sold_individually',
			'_weight'                => 'weight',
			'_length'                => 'length',
			'_width'                 => 'width',
			'_height'                => 'height',
			'_upsell_ids'            => 'upsell_ids',
			'_crosssell_ids'         => 'cross_sell_ids',
			'_purchase_note'         => 'purchase_note',
			'_default_attributes'    => 'default_attributes',
			'_virtual'               => 'virtual',
			'_downloadable'          => 'downloadable',
			'_product_image_gallery' => 'gallery_image_ids',
			'_download_limit'        => 'download_limit',
			'_download_expiry'       => 'download_expiry',
			'_thumbnail_id'          => 'image_id',
			'_stock'                 => 'stock_quantity',
			'_stock_status'          => 'stock_status',
			'_wc_average_rating'     => 'average_rating',
			'_wc_rating_count'       => 'rating_counts',
			'_wc_review_count'       => 'review_count',
		);

		// Make sure to take extra data (like product url or text for external products) into account.
		$extra_data_keys = $product->get_extra_data_keys();

		foreach ( $extra_data_keys as $key ) {
			$meta_key_to_props[ '_' . $key ] = $key;
		}

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $product->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'virtual':
				case 'downloadable':
				case 'manage_stock':
				case 'sold_individually':
					$value = wc_bool_to_string( $value );
					break;
				case 'gallery_image_ids':
					$value = implode( ',', $value );
					break;
				case 'date_on_sale_from':
				case 'date_on_sale_to':
					$value = $value ? $value->getTimestamp() : '';
					break;
				case 'stock_quantity':
					// Fire actions to let 3rd parties know the stock is about to be changed.
					if ( $product->is_type( 'variation' ) ) {
						/**
						* Action to signal that the value of 'stock_quantity' for a variation is about to change.
						*
						* @since 4.9
						*
						* @param int $product The variation whose stock is about to change.
						*/
						do_action( 'woocommerce_variation_before_set_stock', $product );
					} else {
						/**
						* Action to signal that the value of 'stock_quantity' for a product is about to change.
						*
						* @since 4.9
						*
						* @param int $product The product whose stock is about to change.
						*/
						do_action( 'woocommerce_product_before_set_stock', $product );
					}
					break;
			}

			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		// Update extra data associated with the product like button text or product URL for external products.
		if ( ! $this->extra_data_saved ) {
			foreach ( $extra_data_keys as $key ) {
				$meta_key = '_' . $key;
				$function = 'get_' . $key;
				if ( ! array_key_exists( $meta_key, $props_to_update ) ) {
					continue;
				}
				if ( is_callable( array( $product, $function ) ) ) {
					$value   = $product->{$function}( 'edit' );
					$value   = is_string( $value ) ? wp_slash( $value ) : $value;
					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

					if ( $updated ) {
						$this->updated_props[] = $key;
					}
				}
			}
		}

		if ( $this->update_downloads( $product, $force ) ) {
			$this->updated_props[] = 'downloads';
		}
	}

	/**
	 * Handle updated meta props after updating meta data.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product Object.
	 */
	protected function handle_updated_props( &$product ) {
		$price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );

		if ( ! $price_is_synced ) {
			if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
				if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
					update_post_meta( $product->get_id(), '_sale_price', '' );
					$product->set_sale_price( '' );
				}
			}

			if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) {
				if ( $product->is_on_sale( 'edit' ) ) {
					update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
					$product->set_price( $product->get_sale_price( 'edit' ) );
				} else {
					update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
					$product->set_price( $product->get_regular_price( 'edit' ) );
				}
			}
		}

		if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_variation_set_stock', $product );
			} else {
				do_action( 'woocommerce_product_set_stock', $product );
			}
		}

		if ( in_array( 'stock_status', $this->updated_props, true ) ) {
			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
			} else {
				do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
			}
		}

		if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) {
			$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
		}

		// Trigger action so 3rd parties can deal with updated props.
		do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );

		// After handling, we can reset the props array.
		$this->updated_props = array();
	}

	/**
	 * For all stored terms in all taxonomies, save them to the DB.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_terms( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'category_ids', $changes ) ) {
			$categories = $product->get_category_ids( 'edit' );

			if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
				$categories = array( get_option( 'default_product_cat', 0 ) );
			}

			wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
		}
		if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
			wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
		}
		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
		}

		_wc_recount_terms_by_product( $product->get_id() );
	}

	/**
	 * Update visibility terms based on props.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_visibility( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
			$terms = array();

			if ( $product->get_featured() ) {
				$terms[] = 'featured';
			}

			if ( 'outofstock' === $product->get_stock_status() ) {
				$terms[] = 'outofstock';
			}

			$rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) );

			if ( $rating > 0 ) {
				$terms[] = 'rated-' . $rating;
			}

			switch ( $product->get_catalog_visibility() ) {
				case 'hidden':
					$terms[] = 'exclude-from-search';
					$terms[] = 'exclude-from-catalog';
					break;
				case 'catalog':
					$terms[] = 'exclude-from-search';
					break;
				case 'search':
					$terms[] = 'exclude-from-catalog';
					break;
			}

			if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
				do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
			}
		}
	}

	/**
	 * Update attributes which are a mix of terms and meta data.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_attributes( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'attributes', $changes ) ) {
			$attributes  = $product->get_attributes();
			$meta_values = array();

			if ( $attributes ) {
				foreach ( $attributes as $attribute_key => $attribute ) {
					$value = '';

					if ( is_null( $attribute ) ) {
						if ( taxonomy_exists( $attribute_key ) ) {
							// Handle attributes that have been unset.
							wp_set_object_terms( $product->get_id(), array(), $attribute_key );
						} elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) {
							// Handle attributes that have been unset.
							wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) );
						}
						continue;

					} elseif ( $attribute->is_taxonomy() ) {
						wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
					} else {
						$value = wc_implode_text_attributes( $attribute->get_options() );
					}

					// Store in format WC uses in meta.
					$meta_values[ $attribute_key ] = array(
						'name'         => $attribute->get_name(),
						'value'        => $value,
						'position'     => $attribute->get_position(),
						'is_visible'   => $attribute->get_visible() ? 1 : 0,
						'is_variation' => $attribute->get_variation() ? 1 : 0,
						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
					);
				}
			}
			// Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround.
			$this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) );
		}
	}

	/**
	 * Update downloads.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @return bool If updated or not.
	 */
	protected function update_downloads( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'downloads', $changes ) ) {
			$downloads   = $product->get_downloads();
			$meta_values = array();

			if ( $downloads ) {
				foreach ( $downloads as $key => $download ) {
					// Store in format WC uses in meta.
					$meta_values[ $key ] = $download->get_data();
				}
			}

			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
			} else {
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
			}

			return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) );
		}
		return false;
	}

	/**
	 * Make sure we store the product type and version (to track data changes).
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function update_version_and_type( &$product ) {
		$old_type = WC_Product_Factory::get_product_type( $product->get_id() );
		$new_type = $product->get_type();

		wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
		update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );

		// Action for the transition.
		if ( $old_type !== $new_type ) {
			$this->updated_props[] = 'product_type';
			do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
		}
	}

	/**
	 * Clear any caches.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function clear_caches( &$product ) {
		wc_delete_product_transients( $product->get_id() );
		if ( $product->get_parent_id( 'edit' ) ) {
			wc_delete_product_transients( $product->get_parent_id( 'edit' ) );
			WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) );
		}
		WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) );
		WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() );
	}

	/*
	|--------------------------------------------------------------------------
	| wc-product-functions.php methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns an array of on sale products, as an array of objects with an
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
	 *
	 * @return array
	 * @since 3.0.0
	 */
	public function get_on_sale_products() {
		global $wpdb;

		$exclude_term_ids            = array();
		$outofstock_join             = '';
		$outofstock_where            = '';
		$non_published_where         = '';
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		if ( count( $exclude_term_ids ) ) {
			$outofstock_join  = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_results(
			"
			SELECT posts.ID as id, posts.post_parent as parent_id
			FROM {$wpdb->posts} AS posts
			INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
			$outofstock_join
			WHERE posts.post_type IN ( 'product', 'product_variation' )
			AND posts.post_status = 'publish'
			AND lookup.onsale = 1
			$outofstock_where
			AND posts.post_parent NOT IN (
				SELECT ID FROM `$wpdb->posts` as posts
				WHERE posts.post_type = 'product'
				AND posts.post_parent = 0
				AND posts.post_status != 'publish'
			)
			GROUP BY posts.ID
			"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Returns a list of product IDs ( id as key => parent as value) that are
	 * featured. Uses get_posts instead of wc_get_products since we want
	 * some extra meta queries and ALL products (posts_per_page = -1).
	 *
	 * @return array
	 * @since 3.0.0
	 */
	public function get_featured_product_ids() {
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		return get_posts(
			array(
				'post_type'      => array( 'product', 'product_variation' ),
				'posts_per_page' => -1,
				'post_status'    => 'publish',
				'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
					'relation' => 'AND',
					array(
						'taxonomy' => 'product_visibility',
						'field'    => 'term_taxonomy_id',
						'terms'    => array( $product_visibility_term_ids['featured'] ),
					),
					array(
						'taxonomy' => 'product_visibility',
						'field'    => 'term_taxonomy_id',
						'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
						'operator' => 'NOT IN',
					),
				),
				'fields'         => 'id=>parent',
			)
		);
	}

	/**
	 * Check if product sku is found for any other product IDs.
	 *
	 * @since 3.0.0
	 * @param int    $product_id Product ID.
	 * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
	 * @return bool
	 */
	public function is_existing_sku( $product_id, $sku ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return (bool) $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT posts.ID
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE
				posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status != 'trash'
				AND lookup.sku = %s
				AND lookup.product_id <> %d
				LIMIT 1
				",
				wp_slash( $sku ),
				$product_id
			)
		);
	}

	/**
	 * Return product ID based on SKU.
	 *
	 * @since 3.0.0
	 * @param string $sku Product SKU.
	 * @return int
	 */
	public function get_product_id_by_sku( $sku ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$id = $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT posts.ID
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE
				posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status != 'trash'
				AND lookup.sku = %s
				LIMIT 1
				",
				$sku
			)
		);

		return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
	}

	/**
	 * Returns an array of IDs of products that have sales starting soon.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_starting_sales() {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
				WHERE postmeta.meta_key = '_sale_price_dates_from'
					AND postmeta_2.meta_key = '_price'
					AND postmeta_3.meta_key = '_sale_price'
					AND postmeta.meta_value > 0
					AND postmeta.meta_value < %s
					AND postmeta_2.meta_value != postmeta_3.meta_value",
				time()
			)
		);
	}

	/**
	 * Returns an array of IDs of products that have sales which are due to end.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_ending_sales() {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
				WHERE postmeta.meta_key = '_sale_price_dates_to'
					AND postmeta_2.meta_key = '_price'
					AND postmeta_3.meta_key = '_regular_price'
					AND postmeta.meta_value > 0
					AND postmeta.meta_value < %s
					AND postmeta_2.meta_value != postmeta_3.meta_value",
				time()
			)
		);
	}

	/**
	 * Find a matching (enabled) variation within a variable product.
	 *
	 * @since  3.0.0
	 * @param  WC_Product $product Variable product.
	 * @param  array      $match_attributes Array of attributes we want to try to match.
	 * @return int Matching variation ID or 0.
	 */
	public function find_matching_product_variation( $product, $match_attributes = array() ) {
		global $wpdb;

		$meta_attribute_names = array();

		// Get attributes to match in meta.
		foreach ( $product->get_attributes() as $attribute ) {
			if ( ! $attribute->get_variation() ) {
				continue;
			}
			$meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() );
		}

		// Get the attributes of the variations.
		$query = $wpdb->prepare(
			"
			SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta
			LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID
			WHERE postmeta.post_id IN (
				SELECT ID FROM {$wpdb->posts}
				WHERE {$wpdb->posts}.post_parent = %d
				AND {$wpdb->posts}.post_status = 'publish'
				AND {$wpdb->posts}.post_type = 'product_variation'
			)
			",
			$product->get_id()
		);

		$query .= " AND postmeta.meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $meta_attribute_names ) ) . "' )";

		$query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;';

		$attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( ! $attributes ) {
			return 0;
		}

		$sorted_meta = array();

		foreach ( $attributes as $m ) {
			$sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		}

		/**
		 * Check each variation to find the one that matches the $match_attributes.
		 *
		 * Note: Not all meta fields will be set which is why we check existence.
		 */
		foreach ( $sorted_meta as $variation_id => $variation ) {
			$match = true;

			// Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use.
			foreach ( $variation as $attribute_key => $attribute_value ) {
				$match_any_value = '' === $attribute_value;

				if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) {
					$match = false; // Requires a selection but no value was provide.
				}

				if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided.
					if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) {
						$match = false; // Provided value does not match variation.
					}
				}
			}

			if ( true === $match ) {
				return $variation_id;
			}
		}

		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
			/**
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
			 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
			 */
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
		}

		return 0;
	}

	/**
	 * Creates all possible combinations of variations from the attributes, without creating duplicates.
	 *
	 * @since  3.6.0
	 * @todo   Add to interface in 4.0.
	 * @param  WC_Product $product Variable product.
	 * @param  int        $limit Limit the number of created variations.
	 * @return int        Number of created variations.
	 */
	public function create_all_product_variations( $product, $limit = -1 ) {
		$count = 0;

		if ( ! $product ) {
			return $count;
		}

		$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );

		if ( empty( $attributes ) ) {
			return $count;
		}

		// Get existing variations so we don't create duplicates.
		$existing_variations = array_map( 'wc_get_product', $product->get_children() );
		$existing_attributes = array();

		foreach ( $existing_variations as $existing_variation ) {
			$existing_attributes[] = $existing_variation->get_attributes();
		}

		$possible_attributes = array_reverse( wc_array_cartesian( $attributes ) );

		foreach ( $possible_attributes as $possible_attribute ) {
			// Allow any order if key/values -- do not use strict mode.
			if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
				continue;
			}
			$variation = wc_get_product_object( 'variation' );
			$variation->set_parent_id( $product->get_id() );
			$variation->set_attributes( $possible_attribute );
			$variation_id = $variation->save();

			do_action( 'product_variation_linked', $variation_id );

			$count ++;

			if ( $limit > 0 && $count >= $limit ) {
				break;
			}
		}

		return $count;
	}

	/**
	 * Make sure all variations have a sort order set so they can be reordered correctly.
	 *
	 * @param int $parent_id Product ID.
	 */
	public function sort_all_product_variations( $parent_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$ids   = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status in ( 'publish', 'private' ) ORDER BY menu_order ASC, ID ASC",
				$parent_id
			)
		);
		$index = 1;

		foreach ( $ids as $id ) {
			// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
			$wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) );
		}
	}

	/**
	 * Return a list of related products (using data like categories and IDs).
	 *
	 * @since 3.0.0
	 * @param array $cats_array  List of categories IDs.
	 * @param array $tags_array  List of tags IDs.
	 * @param array $exclude_ids Excluded IDs.
	 * @param int   $limit       Limit of results.
	 * @param int   $product_id  Product ID.
	 * @return array
	 */
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
		global $wpdb;

		$args = array(
			'categories'  => $cats_array,
			'tags'        => $tags_array,
			'exclude_ids' => $exclude_ids,
			'limit'       => $limit + 10,
		);

		$related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_col( implode( ' ', $related_product_query ) );
	}

	/**
	 * Builds the related posts query.
	 *
	 * @since 3.0.0
	 *
	 * @param array $cats_array  List of categories IDs.
	 * @param array $tags_array  List of tags IDs.
	 * @param array $exclude_ids Excluded IDs.
	 * @param int   $limit       Limit of results.
	 *
	 * @return array
	 */
	public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
		global $wpdb;

		$include_term_ids            = array_merge( $cats_array, $tags_array );
		$exclude_term_ids            = array();
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
		}

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		$query = array(
			'fields' => "
				SELECT DISTINCT ID FROM {$wpdb->posts} p
			",
			'join'   => '',
			'where'  => "
				WHERE 1=1
				AND p.post_status = 'publish'
				AND p.post_type = 'product'

			",
			'limits' => '
				LIMIT ' . absint( $limit ) . '
			',
		);

		if ( count( $exclude_term_ids ) ) {
			$query['join']  .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
			$query['where'] .= ' AND exclude_join.object_id IS NULL';
		}

		if ( count( $include_term_ids ) ) {
			$query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID';
		}

		if ( count( $exclude_ids ) ) {
			$query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
		}

		return $query;
	}

	/**
	 * Update a product's stock amount directly in the database.
	 *
	 * Updates both post meta and lookup tables. Ignores manage stock setting on the product.
	 *
	 * @param int            $product_id_with_stock Product ID.
	 * @param int|float|null $stock_quantity        Stock quantity.
	 */
	protected function set_product_stock( $product_id_with_stock, $stock_quantity ) {
		global $wpdb;

		// Generate SQL.
		$sql = $wpdb->prepare(
			"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
			$stock_quantity,
			$product_id_with_stock
		);

		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' );

		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared

		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
		wp_cache_delete( $product_id_with_stock, 'post_meta' );

		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );
	}

	/**
	 * Update a product's stock amount directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
	 * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables.
	 * Uses locking to update the quantity. If the lock is not acquired, change is lost.
	 *
	 * @since  3.0.0 this supports set, increase and decrease.
	 * @param  int            $product_id_with_stock Product ID.
	 * @param  int|float|null $stock_quantity Stock quantity.
	 * @param  string         $operation Set, increase and decrease.
	 * @return int|float New stock level.
	 */
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
		global $wpdb;

		// Ensures a row exists to update.
		add_post_meta( $product_id_with_stock, '_stock', 0, true );

		if ( 'set' === $operation ) {
			$new_stock = wc_stock_amount( $stock_quantity );

			// Generate SQL.
			$sql = $wpdb->prepare(
				"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
				$new_stock,
				$product_id_with_stock
			);
		} else {
			$current_stock = wc_stock_amount(
				$wpdb->get_var(
					$wpdb->prepare(
						"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';",
						$product_id_with_stock
					)
				)
			);

			// Calculate new value for filter below. Set multiplier to subtract or add the meta_value.
			switch ( $operation ) {
				case 'increase':
					$new_stock  = $current_stock + wc_stock_amount( $stock_quantity );
					$multiplier = 1;
					break;
				default:
					$new_stock  = $current_stock - wc_stock_amount( $stock_quantity );
					$multiplier = -1;
					break;
			}

			// Generate SQL.
			$sql = $wpdb->prepare(
				"UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'",
				wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation.
				$product_id_with_stock
			);
		}

		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation );

		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared

		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
		wp_cache_delete( $product_id_with_stock, 'post_meta' );

		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id_with_stock Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_stock', $product_id_with_stock );

		return $new_stock;
	}

	/**
	 * Update a product's sale count directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
	 *
	 * @since  3.0.0 this supports set, increase and decrease.
	 * @param  int      $product_id Product ID.
	 * @param  int|null $quantity Quantity.
	 * @param  string   $operation set, increase and decrease.
	 */
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
		global $wpdb;
		add_post_meta( $product_id, 'total_sales', 0, true );

		// Update stock in DB directly.
		switch ( $operation ) {
			case 'increase':
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
			case 'decrease':
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
			default:
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
		}

		wp_cache_delete( $product_id, 'post_meta' );

		$this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_sales', $product_id );
	}

	/**
	 * Update a products average rating meta.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_average_rating( $product ) {
		update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
		self::update_visibility( $product, true );
	}

	/**
	 * Update a products review count meta.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_review_count( $product ) {
		update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
	}

	/**
	 * Update a products rating counts.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_rating_counts( $product ) {
		update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
	}

	/**
	 * Get shipping class ID by slug.
	 *
	 * @since 3.0.0
	 * @param string $slug Product shipping class slug.
	 * @return int|false
	 */
	public function get_shipping_class_id_by_slug( $slug ) {
		$shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
		if ( $shipping_class_term ) {
			return $shipping_class_term->term_id;
		} else {
			return false;
		}
	}

	/**
	 * Returns an array of products.
	 *
	 * @param  array $args Args to pass to WC_Product_Query().
	 * @return array|object
	 * @see wc_get_products
	 */
	public function get_products( $args = array() ) {
		$query = new WC_Product_Query( $args );
		return $query->get_products();
	}

	/**
	 * Search product data for a term and return ids.
	 *
	 * @param  string     $term Search term.
	 * @param  string     $type Type of product.
	 * @param  bool       $include_variations Include variations in search or not.
	 * @param  bool       $all_statuses Should we search all statuses or limit to published.
	 * @param  null|int   $limit Limit returned results. @since 3.5.0.
	 * @param  null|array $include Keep specific results. @since 3.6.0.
	 * @param  null|array $exclude Discard specific results. @since 3.6.0.
	 * @return array of ids
	 */
	public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) {
		global $wpdb;

		$custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit );

		if ( is_array( $custom_results ) ) {
			return $custom_results;
		}

		$post_types   = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
		$join_query   = '';
		$type_where   = '';
		$status_where = '';
		$limit_query  = '';

		// When searching variations we should include the parent's meta table for use in searches.
		if ( $include_variations ) {
			$join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup
			 ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent ";
		}

		/**
		 * Hook woocommerce_search_products_post_statuses.
		 *
		 * @since 3.7.0
		 * @param array $post_statuses List of post statuses.
		 */
		$post_statuses = apply_filters(
			'woocommerce_search_products_post_statuses',
			current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' )
		);

		// See if search term contains OR keywords.
		if ( stristr( $term, ' or ' ) ) {
			$term_groups = preg_split( '/\s+or\s+/i', $term );
		} else {
			$term_groups = array( $term );
		}

		$search_where   = '';
		$search_queries = array();

		foreach ( $term_groups as $term_group ) {
			// Parse search terms.
			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
				$search_terms = $this->get_valid_search_terms( $matches[0] );
				$count        = count( $search_terms );

				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
				if ( 9 < $count || 0 === $count ) {
					$search_terms = array( $term_group );
				}
			} else {
				$search_terms = array( $term_group );
			}

			$term_group_query = '';
			$searchand        = '';

			foreach ( $search_terms as $search_term ) {
				$like = '%' . $wpdb->esc_like( $search_term ) . '%';

				// Variations should also search the parent's meta table for fallback fields.
				if ( $include_variations ) {
					$variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like );
				} else {
					$variation_query = '';
				}

				$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$searchand         = ' AND ';
			}

			if ( $term_group_query ) {
				$search_queries[] = $term_group_query;
			}
		}

		if ( ! empty( $search_queries ) ) {
			$search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') ';
		}

		if ( ! empty( $include ) && is_array( $include ) ) {
			$search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') ';
		}

		if ( ! empty( $exclude ) && is_array( $exclude ) ) {
			$search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') ';
		}

		if ( 'virtual' === $type ) {
			$type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) ';
		} elseif ( 'downloadable' === $type ) {
			$type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) ';
		}

		if ( ! $all_statuses ) {
			$status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
		}

		if ( $limit ) {
			$limit_query = $wpdb->prepare( ' LIMIT %d ', $limit );
		}

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$search_results = $wpdb->get_results(
			// phpcs:disable
			"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
			 LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
			 $join_query
			WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
			$search_where
			$status_where
			$type_where
			ORDER BY posts.post_parent ASC, posts.post_title ASC
			$limit_query
			"
			// phpcs:enable
		);

		$product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );

		if ( is_numeric( $term ) ) {
			$post_id   = absint( $term );
			$post_type = get_post_type( $post_id );

			if ( 'product_variation' === $post_type && $include_variations ) {
				$product_ids[] = $post_id;
			} elseif ( 'product' === $post_type ) {
				$product_ids[] = $post_id;
			}

			$product_ids[] = wp_get_post_parent_id( $post_id );
		}

		return wp_parse_id_list( $product_ids );
	}

	/**
	 * Get the product type based on product ID.
	 *
	 * @since 3.0.0
	 * @param int $product_id Product ID.
	 * @return bool|string
	 */
	public function get_product_type( $product_id ) {
		$cache_key    = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
		$product_type = wp_cache_get( $cache_key, 'products' );

		if ( $product_type ) {
			return $product_type;
		}

		$post_type = get_post_type( $product_id );

		if ( 'product_variation' === $post_type ) {
			$product_type = 'variation';
		} elseif ( 'product' === $post_type ) {
			$terms        = get_the_terms( $product_id, 'product_type' );
			$product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
		} else {
			$product_type = false;
		}

		wp_cache_set( $cache_key, $product_type, 'products' );

		return $product_type;
	}

	/**
	 * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
	 *
	 * @since 3.2.0
	 * @param string   $where Where clause.
	 * @param WP_Query $wp_query WP_Query instance.
	 * @return string
	 */
	public function reviews_allowed_query_where( $where, $wp_query ) {
		global $wpdb;

		if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
			if ( $wp_query->query_vars['reviews_allowed'] ) {
				$where .= " AND $wpdb->posts.comment_status = 'open'";
			} else {
				$where .= " AND $wpdb->posts.comment_status = 'closed'";
			}
		}

		return $where;
	}

	/**
	 * Get valid WP_Query args from a WC_Product_Query's query variables.
	 *
	 * @since 3.2.0
	 * @param array $query_vars Query vars from a WC_Product_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
		$key_mapping = array(
			'status'         => 'post_status',
			'page'           => 'paged',
			'include'        => 'post__in',
			'stock_quantity' => 'stock',
			'average_rating' => 'wc_average_rating',
			'review_count'   => 'wc_review_count',
		);
		foreach ( $key_mapping as $query_key => $db_key ) {
			if ( isset( $query_vars[ $query_key ] ) ) {
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
				unset( $query_vars[ $query_key ] );
			}
		}

		// Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
		$boolean_queries = array(
			'virtual',
			'downloadable',
			'sold_individually',
			'manage_stock',
		);
		foreach ( $boolean_queries as $boolean_query ) {
			if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
				$query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
			}
		}

		// These queries cannot be auto-generated so we have to remove them and build them manually.
		$manual_queries = array(
			'sku'        => '',
			'featured'   => '',
			'visibility' => '',
		);
		foreach ( $manual_queries as $key => $manual_query ) {
			if ( isset( $query_vars[ $key ] ) ) {
				$manual_queries[ $key ] = $query_vars[ $key ];
				unset( $query_vars[ $key ] );
			}
		}

		$wp_query_args = parent::get_wp_query_args( $query_vars );

		if ( ! isset( $wp_query_args['date_query'] ) ) {
			$wp_query_args['date_query'] = array();
		}
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		// Handle product types.
		if ( 'variation' === $query_vars['type'] ) {
			$wp_query_args['post_type'] = 'product_variation';
		} elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
			$wp_query_args['post_type']   = array( 'product_variation', 'product' );
			$wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
				'relation' => 'OR',
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => $query_vars['type'],
				),
				array(
					'taxonomy' => 'product_type',
					'field'    => 'id',
					'operator' => 'NOT EXISTS',
				),
			);
		} else {
			$wp_query_args['post_type']   = 'product';
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $query_vars['type'],
			);
		}

		// Handle product categories.
		if ( ! empty( $query_vars['category'] ) ) {
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_cat',
				'field'    => 'slug',
				'terms'    => $query_vars['category'],
			);
		}

		// Handle product tags.
		if ( ! empty( $query_vars['tag'] ) ) {
			unset( $wp_query_args['tag'] );
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_tag',
				'field'    => 'slug',
				'terms'    => $query_vars['tag'],
			);
		}

		// Handle shipping classes.
		if ( ! empty( $query_vars['shipping_class'] ) ) {
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_shipping_class',
				'field'    => 'slug',
				'terms'    => $query_vars['shipping_class'],
			);
		}

		// Handle total_sales.
		// This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
		if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
			$wp_query_args['meta_query'][] = array(
				'key'     => 'total_sales',
				'value'   => absint( $query_vars['total_sales'] ),
				'compare' => '=',
			);
		}

		// Handle SKU.
		if ( $manual_queries['sku'] ) {
			// Check for existing values if wildcard is used.
			if ( '*' === $manual_queries['sku'] ) {
				$wp_query_args['meta_query'][] = array(
					array(
						'key'     => '_sku',
						'compare' => 'EXISTS',
					),
					array(
						'key'     => '_sku',
						'value'   => '',
						'compare' => '!=',
					),
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => '_sku',
					'value'   => $manual_queries['sku'],
					'compare' => 'LIKE',
				);
			}
		}

		// Handle featured.
		if ( '' !== $manual_queries['featured'] ) {
			$product_visibility_term_ids = wc_get_product_visibility_term_ids();
			if ( $manual_queries['featured'] ) {
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['featured'] ),
				);
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
					'operator' => 'NOT IN',
				);
			} else {
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['featured'] ),
					'operator' => 'NOT IN',
				);
			}
		}

		// Handle visibility.
		if ( $manual_queries['visibility'] ) {
			switch ( $manual_queries['visibility'] ) {
				case 'search':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-search' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'catalog':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'visible':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'hidden':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
						'operator' => 'AND',
					);
					break;
			}
		}

		// Handle date queries.
		$date_queries = array(
			'date_created'      => 'post_date',
			'date_modified'     => 'post_modified',
			'date_on_sale_from' => '_sale_price_dates_from',
			'date_on_sale_to'   => '_sale_price_dates_to',
		);
		foreach ( $date_queries as $query_var_key => $db_key ) {
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {

				// Remove any existing meta queries for the same keys to prevent conflicts.
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
				foreach ( $existing_queries as $query_index => $query_contents ) {
					unset( $wp_query_args['meta_query'][ $query_index ] );
				}

				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
			}
		}

		// Handle paginate.
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$wp_query_args['no_found_rows'] = true;
		}

		// Handle reviews_allowed.
		if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
			add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
		}

		// Handle orderby.
		if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) {
			$wp_query_args['orderby'] = 'post__in';
		}

		return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
	}

	/**
	 * Query for Products matching specific criteria.
	 *
	 * @since 3.2.0
	 *
	 * @param array $query_vars Query vars from a WC_Product_Query.
	 *
	 * @return array|object
	 */
	public function query( $query_vars ) {
		$args = $this->get_wp_query_args( $query_vars );

		if ( ! empty( $args['errors'] ) ) {
			$query = (object) array(
				'posts'         => array(),
				'found_posts'   => 0,
				'max_num_pages' => 0,
			);
		} else {
			$query = new WP_Query( $args );
		}

		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
			// Prime caches before grabbing objects.
			update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
		}

		$products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'products'      => $products,
				'total'         => $query->found_posts,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $products;
	}

	/**
	 * Get data to save to a lookup table.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 * @return array
	 */
	protected function get_data_for_lookup_table( $id, $table ) {
		if ( 'wc_product_meta_lookup' === $table ) {
			$price_meta   = (array) get_post_meta( $id, '_price', false );
			$manage_stock = get_post_meta( $id, '_manage_stock', true );
			$stock        = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
			$price        = wc_format_decimal( get_post_meta( $id, '_price', true ) );
			$sale_price   = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
			return array(
				'product_id'     => absint( $id ),
				'sku'            => get_post_meta( $id, '_sku', true ),
				'virtual'        => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
				'downloadable'   => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
				'min_price'      => reset( $price_meta ),
				'max_price'      => end( $price_meta ),
				'onsale'         => $sale_price && $price === $sale_price ? 1 : 0,
				'stock_quantity' => $stock,
				'stock_status'   => get_post_meta( $id, '_stock_status', true ),
				'rating_count'   => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ),
				'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
				'total_sales'    => get_post_meta( $id, 'total_sales', true ),
				'tax_status'     => get_post_meta( $id, '_tax_status', true ),
				'tax_class'      => get_post_meta( $id, '_tax_class', true ),
			);
		}
		return array();
	}

	/**
	 * Get primary key name for lookup table.
	 *
	 * @since 3.6.0
	 * @param string $table Lookup table name.
	 * @return string
	 */
	protected function get_primary_key_for_lookup_table( $table ) {
		if ( 'wc_product_meta_lookup' === $table ) {
			return 'product_id';
		}
		return '';
	}

	/**
	 * Returns query statement for getting current `_stock` of a product.
	 *
	 * @internal MAX function below is used to make sure result is a scalar.
	 * @param int $product_id Product ID.
	 * @return string|void Query statement.
	 */
	public function get_query_for_stock( $product_id ) {
		global $wpdb;
		return $wpdb->prepare(
			"
			SELECT COALESCE( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table
			WHERE meta_table.meta_key = '_stock'
			AND meta_table.post_id = %d
			",
			$product_id
		);
	}
}
class-wc-product-grouped-data-store-cpt.php000064400000005422151542600450014744 0ustar00<?php
/**
 * Class WC_Product_Grouped_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Grouped Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {

	/**
	 * Helper method that updates all the post meta for a grouped product.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_children' => 'children',
		);

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value   = $product->{"get_$prop"}( 'edit' );
			$updated = update_post_meta( $product->get_id(), $meta_key, $value );
			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		parent::update_post_meta( $product, $force );
	}

	/**
	 * Handle updated meta props after updating meta data.
	 *
	 * @since  3.0.0
	 * @param  WC_Product $product Product object.
	 */
	protected function handle_updated_props( &$product ) {
		if ( in_array( 'children', $this->updated_props, true ) ) {
			$this->update_prices_from_children( $product );
		}
		parent::handle_updated_props( $product );
	}

	/**
	 * Sync grouped product prices with children.
	 *
	 * @since 3.0.0
	 * @param WC_Product|int $product Product object or product ID.
	 */
	public function sync_price( &$product ) {
		$this->update_prices_from_children( $product );
	}

	/**
	 * Loop over child products and update the grouped product prices.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function update_prices_from_children( &$product ) {
		$child_prices = array();
		foreach ( $product->get_children( 'edit' ) as $child_id ) {
			$child = wc_get_product( $child_id );
			if ( $child ) {
				$child_prices[] = $child->get_price( 'edit' );
			}
		}
		$child_prices = array_filter( $child_prices );
		delete_post_meta( $product->get_id(), '_price' );
		delete_post_meta( $product->get_id(), '_sale_price' );
		delete_post_meta( $product->get_id(), '_regular_price' );

		if ( ! empty( $child_prices ) ) {
			add_post_meta( $product->get_id(), '_price', min( $child_prices ) );
			add_post_meta( $product->get_id(), '_price', max( $child_prices ) );
		}

		$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_price', $product->get_id() );
	}
}
class-wc-product-variable-data-store-cpt.php000064400000057170151542600450015073 0ustar00<?php
/**
 * File for WC Variable Product Data Store class.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Variable Product Data Store: Stored in CPT.
 *
 * @version 3.0.0
 */
class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {

	/**
	 * Cached & hashed prices array for child variations.
	 *
	 * @var array
	 */
	protected $prices_array = array();

	/**
	 * Read attributes from post meta.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function read_attributes( &$product ) {
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );

		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
			$attributes   = array();
			$force_update = false;
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
				$meta_value = array_merge(
					array(
						'name'         => '',
						'value'        => '',
						'position'     => 0,
						'is_visible'   => 0,
						'is_variation' => 0,
						'is_taxonomy'  => 0,
					),
					(array) $meta_attribute_value
				);

				// Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly.
				if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
					global $wpdb;

					$old_slug      = 'attribute_' . $meta_attribute_key;
					$new_slug      = 'attribute_' . sanitize_title( $meta_value['name'] );
					$old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok.

					if ( $old_meta_rows ) {
						foreach ( $old_meta_rows as $old_meta_row ) {
							update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value );
						}
					}

					$force_update = true;
				}

				// Check if is a taxonomy attribute.
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
						continue;
					}
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
				} else {
					$id      = 0;
					$options = wc_get_text_attributes( $meta_value['value'] );
				}

				$attribute = new WC_Product_Attribute();
				$attribute->set_id( $id );
				$attribute->set_name( $meta_value['name'] );
				$attribute->set_options( $options );
				$attribute->set_position( $meta_value['position'] );
				$attribute->set_visible( $meta_value['is_visible'] );
				$attribute->set_variation( $meta_value['is_variation'] );
				$attributes[] = $attribute;
			}
			$product->set_attributes( $attributes );

			if ( $force_update ) {
				$this->update_attributes( $product, true );
			}
		}
	}

	/**
	 * Read product data.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	protected function read_product_data( &$product ) {
		parent::read_product_data( $product );

		// Make sure data which does not apply to variables is unset.
		$product->set_regular_price( '' );
		$product->set_sale_price( '' );
	}

	/**
	 * Loads variation child IDs.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force_read True to bypass the transient.
	 *
	 * @return array
	 */
	public function read_children( &$product, $force_read = false ) {
		$children_transient_name = 'wc_product_children_' . $product->get_id();
		$children                = get_transient( $children_transient_name );
		if ( empty( $children ) || ! is_array( $children ) ) {
			$children = array();
		}

		if ( ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
			$all_args = array(
				'post_parent' => $product->get_id(),
				'post_type'   => 'product_variation',
				'orderby'     => array(
					'menu_order' => 'ASC',
					'ID'         => 'ASC',
				),
				'fields'      => 'ids',
				'post_status' => array( 'publish', 'private' ),
				'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
			);

			$visible_only_args                = $all_args;
			$visible_only_args['post_status'] = 'publish';

			if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
				$visible_only_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'name',
					'terms'    => 'outofstock',
					'operator' => 'NOT IN',
				);
			}
			$children['all']     = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) );
			$children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) );

			set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 );
		}

		$children['all']     = wp_parse_id_list( (array) $children['all'] );
		$children['visible'] = wp_parse_id_list( (array) $children['visible'] );

		return $children;
	}

	/**
	 * Loads an array of attributes used for variations, as well as their possible values.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @return array
	 */
	public function read_variation_attributes( &$product ) {
		global $wpdb;

		$variation_attributes = array();
		$attributes           = $product->get_attributes();
		$child_ids            = $product->get_children();
		$cache_key            = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id();
		$cache_group          = 'products';
		$cached_data          = wp_cache_get( $cache_key, $cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		if ( ! empty( $attributes ) ) {
			foreach ( $attributes as $attribute ) {
				if ( empty( $attribute['is_variation'] ) ) {
					continue;
				}

				// Get possible values for this attribute, for only visible variations.
				if ( ! empty( $child_ids ) ) {
					$format     = array_fill( 0, count( $child_ids ), '%d' );
					$query_in   = '(' . implode( ',', $format ) . ')';
					$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
					$values     = array_unique(
						$wpdb->get_col(
							$wpdb->prepare(
								"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
								$query_args
							)
						)
					);
				} else {
					$values = array();
				}

				// Empty value indicates that all options for given attribute are available.
				if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) {
					$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
					// Get custom attributes (non taxonomy) as defined.
				} elseif ( ! $attribute['is_taxonomy'] ) {
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
					$assigned_text_attributes = $values;
					$values                   = array();

					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
					if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
						foreach ( $text_attributes as $text_attribute ) {
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
								$values[] = $text_attribute;
							}
						}
					} else {
						foreach ( $text_attributes as $text_attribute ) {
							if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) {
								$values[] = $text_attribute;
							}
						}
					}
				}
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
			}
		}

		wp_cache_set( $cache_key, $variation_attributes, $cache_group );

		return $variation_attributes;
	}

	/**
	 * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
	 *
	 * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
	 * This is to ensure modified prices are not cached, unless intended.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 *
	 * @return array of prices
	 * @since  3.0.0
	 */
	public function read_price_data( &$product, $for_display = false ) {

		/**
		 * Transient name for storing prices for this product (note: Max transient length is 45)
		 *
		 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
		 */
		$transient_name    = 'wc_var_prices_' . $product->get_id();
		$transient_version = WC_Cache_Helper::get_transient_version( 'product' );
		$price_hash        = $this->get_price_hash( $product, $for_display );

		// Check if prices array is stale.
		if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) {
			$this->prices_array = array(
				'version' => $transient_version,
			);
		}

		/**
		 * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array.
		 * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
		 */
		if ( empty( $this->prices_array[ $price_hash ] ) ) {
			$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );

			// If the product version has changed since the transient was last saved, reset the transient cache.
			if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) {
				$transient_cached_prices_array = array(
					'version' => $transient_version,
				);
			}

			// If the prices are not stored for this hash, generate them and add to the transient.
			if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
				$prices_array = array(
					'price'         => array(),
					'regular_price' => array(),
					'sale_price'    => array(),
				);

				$variation_ids = $product->get_visible_children();

				if ( is_callable( '_prime_post_caches' ) ) {
					_prime_post_caches( $variation_ids );
				}

				foreach ( $variation_ids as $variation_id ) {
					$variation = wc_get_product( $variation_id );

					if ( $variation ) {
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );

						// Skip empty prices.
						if ( '' === $price ) {
							continue;
						}

						// If sale price does not equal price, the product is not yet on sale.
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
							$sale_price = $regular_price;
						}

						// If we are getting prices for display, we need to account for taxes.
						if ( $for_display ) {
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
								$price         = '' === $price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $price,
									)
								);
								$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $regular_price,
									)
								);
								$sale_price    = '' === $sale_price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $sale_price,
									)
								);
							} else {
								$price         = '' === $price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $price,
									)
								);
								$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $regular_price,
									)
								);
								$sale_price    = '' === $sale_price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $sale_price,
									)
								);
							}
						}

						$prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
						$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
						$prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price, wc_get_price_decimals() );

						$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
					}
				}

				// Add all pricing data to the transient array.
				foreach ( $prices_array as $key => $values ) {
					$transient_cached_prices_array[ $price_hash ][ $key ] = $values;
				}

				set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
			}

			/**
			 * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
			 * This value may differ from the transient cache. It is filtered once before storing locally.
			 */
			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
		}
		return $this->prices_array[ $price_hash ];
	}

	/**
	 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
	 * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $for_display If taxes should be calculated or not.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_price_hash( &$product, $for_display = false ) {
		global $wp_filter;

		$price_hash = array( false );

		if ( $for_display && wc_tax_enabled() ) {
			$price_hash = array(
				get_option( 'woocommerce_tax_display_shop', 'excl' ),
				WC_Tax::get_rates(),
				empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(),
			);
		}

		$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );

		foreach ( $filter_names as $filter_name ) {
			if ( ! empty( $wp_filter[ $filter_name ] ) ) {
				$price_hash[ $filter_name ] = array();

				foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
					$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
				}
			}
		}

		return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) );
	}

	/**
	 * Does a child have a weight set?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_has_weight( $product ) {
		global $wpdb;
		$children = $product->get_visible_children();
		if ( ! $children ) {
			return false;
		}

		$format   = array_fill( 0, count( $children ), '%d' );
		$query_in = '(' . implode( ',', $format ) . ')';

		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
	}

	/**
	 * Does a child have dimensions set?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_has_dimensions( $product ) {
		global $wpdb;
		$children = $product->get_visible_children();
		if ( ! $children ) {
			return false;
		}

		$format   = array_fill( 0, count( $children ), '%d' );
		$query_in = '(' . implode( ',', $format ) . ')';

		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
	}

	/**
	 * Is a child in stock?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_is_in_stock( $product ) {
		return $this->child_has_stock_status( $product, 'instock' );
	}

	/**
	 * Does a child have a stock status?
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $status 'instock', 'outofstock', or 'onbackorder'.
	 *
	 * @since  3.3.0
	 * @return boolean
	 */
	public function child_has_stock_status( $product, $status ) {
		global $wpdb;

		$children = $product->get_children();

		if ( $children ) {
			$format     = array_fill( 0, count( $children ), '%d' );
			$query_in   = '(' . implode( ',', $format ) . ')';
			$query_args = array( 'stock_status' => $status ) + $children;
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
			if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) {
				$query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}";
			} else {
				$query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}";
			}
			$children_with_status = $wpdb->get_var(
				$wpdb->prepare(
					$query,
					$query_args
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
		} else {
			$children_with_status = 0;
		}

		return (bool) $children_with_status;
	}

	/**
	 * Syncs all variation names if the parent name is changed.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $previous_name Variation previous name.
	 * @param string     $new_name Variation new name.
	 *
	 * @since 3.0.0
	 */
	public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) {
		if ( $new_name !== $previous_name ) {
			global $wpdb;

			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->posts}
					SET post_title = REPLACE( post_title, %s, %s )
					WHERE post_type = 'product_variation'
					AND post_parent = %d",
					$previous_name ? $previous_name : 'AUTO-DRAFT',
					$new_name,
					$product->get_id()
				)
			);
		}
	}

	/**
	 * Stock managed at the parent level - update children being managed by this product.
	 * This sync function syncs downwards (from parent to child) when the variable product is saved.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_managed_variation_stock_status( &$product ) {
		global $wpdb;

		if ( $product->get_manage_stock() ) {
			$children = $product->get_children();
			$changed  = false;

			if ( $children ) {
				$status           = $product->get_stock_status();
				$format           = array_fill( 0, count( $children ), '%d' );
				$query_in         = '(' . implode( ',', $format ) . ')';
				$managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
				foreach ( $managed_children as $managed_child ) {
					if ( update_post_meta( $managed_child, '_stock_status', $status ) ) {
						$this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' );
						$changed = true;
					}
				}
			}

			if ( $changed ) {
				$children = $this->read_children( $product, true );
				$product->set_children( $children['all'] );
				$product->set_visible_children( $children['visible'] );
			}
		}
	}

	/**
	 * Sync variable product prices with children.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_price( &$product ) {
		global $wpdb;

		$children = $product->get_visible_children();
		if ( $children ) {
			$format   = array_fill( 0, count( $children ), '%d' );
			$query_in = '(' . implode( ',', $format ) . ')';
			$prices   = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
		} else {
			$prices = array();
		}

		delete_post_meta( $product->get_id(), '_price' );
		delete_post_meta( $product->get_id(), '_sale_price' );
		delete_post_meta( $product->get_id(), '_regular_price' );

		if ( $prices ) {
			sort( $prices, SORT_NUMERIC );
			// To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner.
			foreach ( $prices as $price ) {
				if ( is_null( $price ) || '' === $price ) {
					continue;
				}
				add_post_meta( $product->get_id(), '_price', $price, false );
			}
		}

		$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_price', $product->get_id() );
	}

	/**
	 * Sync variable product stock status with children.
	 * Change does not persist unless saved by caller.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_stock_status( &$product ) {
		if ( $product->child_is_in_stock() ) {
			$product->set_stock_status( 'instock' );
		} elseif ( $product->child_is_on_backorder() ) {
			$product->set_stock_status( 'onbackorder' );
		} else {
			$product->set_stock_status( 'outofstock' );
		}
	}

	/**
	 * Delete variations of a product.
	 *
	 * @param int  $product_id Product ID.
	 * @param bool $force_delete False to trash.
	 *
	 * @since 3.0.0
	 */
	public function delete_variations( $product_id, $force_delete = false ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		$variation_ids = wp_parse_id_list(
			get_posts(
				array(
					'post_parent' => $product_id,
					'post_type'   => 'product_variation',
					'fields'      => 'ids',
					'post_status' => array( 'any', 'trash', 'auto-draft' ),
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
				)
			)
		);

		if ( ! empty( $variation_ids ) ) {
			foreach ( $variation_ids as $variation_id ) {
				if ( $force_delete ) {
					do_action( 'woocommerce_before_delete_product_variation', $variation_id );
					wp_delete_post( $variation_id, true );
					do_action( 'woocommerce_delete_product_variation', $variation_id );
				} else {
					wp_trash_post( $variation_id );
					do_action( 'woocommerce_trash_product_variation', $variation_id );
				}
			}
		}

		delete_transient( 'wc_product_children_' . $product_id );
	}

	/**
	 * Untrash variations.
	 *
	 * @param int $product_id Product ID.
	 */
	public function untrash_variations( $product_id ) {
		$variation_ids = wp_parse_id_list(
			get_posts(
				array(
					'post_parent' => $product_id,
					'post_type'   => 'product_variation',
					'fields'      => 'ids',
					'post_status' => 'trash',
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
				)
			)
		);

		if ( ! empty( $variation_ids ) ) {
			foreach ( $variation_ids as $variation_id ) {
				wp_untrash_post( $variation_id );
			}
		}

		delete_transient( 'wc_product_children_' . $product_id );
	}
}
class-wc-product-variation-data-store-cpt.php000064400000050403151542600450015272 0ustar00<?php
/**
 * Class WC_Product_Variation_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Variation Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object.
	 * @return bool false if excluded.
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Reads a product from the database and sets its data to the class.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID.
	 */
	public function read( &$product ) {
		$product->set_defaults();

		if ( ! $product->get_id() ) {
			return;
		}

		$post_object = get_post( $product->get_id() );

		if ( ! $post_object ) {
			return;
		}

		if ( 'product_variation' !== $post_object->post_type ) {
			throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) );
		}

		$product->set_props(
			array(
				'name'              => $post_object->post_title,
				'slug'              => $post_object->post_name,
				'date_created'      => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'     => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'            => $post_object->post_status,
				'menu_order'        => $post_object->menu_order,
				'reviews_allowed'   => 'open' === $post_object->comment_status,
				'parent_id'         => $post_object->post_parent,
				'attribute_summary' => $post_object->post_excerpt,
			)
		);

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$this->read_downloads( $product );
		$this->read_product_data( $product );
		$this->read_extra_data( $product );
		$product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) );

		$updates = array();
		/**
		 * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update.
		 */
		$new_title = $this->generate_product_title( $product );

		if ( $post_object->post_title !== $new_title ) {
			$product->set_name( $new_title );
			$updates = array_merge( $updates, array( 'post_title' => $new_title ) );
		}

		/**
		 * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values.
		 * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated
		 * for respective products when they're read.
		 */
		$new_attribute_summary = $this->generate_attribute_summary( $product );

		if ( $new_attribute_summary !== $post_object->post_excerpt ) {
			$product->set_attribute_summary( $new_attribute_summary );
			$updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) );
		}

		if ( ! empty( $updates ) ) {
			$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) );
			clean_post_cache( $product->get_id() );
		}

		// Set object_read true once all data is read.
		$product->set_object_read( true );
	}

	/**
	 * Create a new product.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 */
	public function create( &$product ) {
		if ( ! $product->get_date_created() ) {
			$product->set_date_created( time() );
		}

		$new_title = $this->generate_product_title( $product );

		if ( $product->get_name( 'edit' ) !== $new_title ) {
			$product->set_name( $new_title );
		}

		$attribute_summary = $this->generate_attribute_summary( $product );
		$product->set_attribute_summary( $attribute_summary );

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_product_variation_data',
				array(
					'post_type'      => 'product_variation',
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
					'post_author'    => get_current_user_id(),
					'post_title'     => $product->get_name( 'edit' ),
					'post_excerpt'   => $product->get_attribute_summary( 'edit' ),
					'post_content'   => '',
					'post_parent'    => $product->get_parent_id(),
					'comment_status' => 'closed',
					'ping_status'    => 'closed',
					'menu_order'     => $product->get_menu_order(),
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
					'post_name'      => $product->get_slug( 'edit' ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$product->set_id( $id );

			$this->update_post_meta( $product, true );
			$this->update_terms( $product, true );
			$this->update_visibility( $product, true );
			$this->update_attributes( $product, true );
			$this->handle_updated_props( $product );

			$product->save_meta_data();
			$product->apply_changes();

			$this->update_version_and_type( $product );
			$this->update_guid( $product );

			$this->clear_caches( $product );

			do_action( 'woocommerce_new_product_variation', $id, $product );
		}
	}

	/**
	 * Updates an existing product.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 */
	public function update( &$product ) {
		$product->save_meta_data();

		if ( ! $product->get_date_created() ) {
			$product->set_date_created( time() );
		}

		$new_title = $this->generate_product_title( $product );

		if ( $product->get_name( 'edit' ) !== $new_title ) {
			$product->set_name( $new_title );
		}

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$changes = $product->get_changes();

		if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) {
			$product->set_attribute_summary( $this->generate_attribute_summary( $product ) );
		}

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_title'        => $product->get_name( 'edit' ),
				'post_excerpt'      => $product->get_attribute_summary( 'edit' ),
				'post_parent'       => $product->get_parent_id( 'edit' ),
				'comment_status'    => 'closed',
				'post_status'       => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
				'menu_order'        => $product->get_menu_order( 'edit' ),
				'post_date'         => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
				'post_type'         => 'product_variation',
				'post_name'         => $product->get_slug( 'edit' ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
				clean_post_cache( $product->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
			}
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

		} else { // Only update post modified time to record this save event.
			$GLOBALS['wpdb']->update(
				$GLOBALS['wpdb']->posts,
				array(
					'post_modified'     => current_time( 'mysql' ),
					'post_modified_gmt' => current_time( 'mysql', 1 ),
				),
				array(
					'ID' => $product->get_id(),
				)
			);
			clean_post_cache( $product->get_id() );
		}

		$this->update_post_meta( $product );
		$this->update_terms( $product );
		$this->update_visibility( $product, true );
		$this->update_attributes( $product );
		$this->handle_updated_props( $product );

		$product->apply_changes();

		$this->update_version_and_type( $product );

		$this->clear_caches( $product );

		do_action( 'woocommerce_update_product_variation', $product->get_id(), $product );
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Generates a title with attribute information for a variation.
	 * Products will get a title of the form "Name - Value, Value" or just "Name".
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @return string
	 */
	protected function generate_product_title( $product ) {
		$attributes = (array) $product->get_attributes();

		// Do not include attributes if the product has 3+ attributes.
		$should_include_attributes = count( $attributes ) < 3;

		// Do not include attributes if an attribute name has 2+ words and the
		// product has multiple attributes.
		if ( $should_include_attributes && 1 < count( $attributes ) ) {
			foreach ( $attributes as $name => $value ) {
				if ( false !== strpos( $name, '-' ) ) {
					$should_include_attributes = false;
					break;
				}
			}
		}

		$should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
		$separator                 = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
		$title_base                = get_post_field( 'post_title', $product->get_parent_id() );
		$title_suffix              = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';

		return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
	}

	/**
	 * Generates attribute summary for the variation.
	 *
	 * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
	 *
	 * @since 3.6.0
	 * @param WC_Product_Variation $product Product variation to generate the attribute summary for.
	 *
	 * @return string
	 */
	protected function generate_attribute_summary( $product ) {
		return wc_get_formatted_variation( $product, true, true );
	}

	/**
	 * Make sure we store the product version (to track data changes).
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function update_version_and_type( &$product ) {
		wp_set_object_terms( $product->get_id(), '', 'product_type' );
		update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );
	}

	/**
	 * Read post data.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status.
	 */
	protected function read_product_data( &$product ) {
		$id = $product->get_id();

		$product->set_props(
			array(
				'description'       => get_post_meta( $id, '_variation_description', true ),
				'regular_price'     => get_post_meta( $id, '_regular_price', true ),
				'sale_price'        => get_post_meta( $id, '_sale_price', true ),
				'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ),
				'date_on_sale_to'   => get_post_meta( $id, '_sale_price_dates_to', true ),
				'manage_stock'      => get_post_meta( $id, '_manage_stock', true ),
				'stock_status'      => get_post_meta( $id, '_stock_status', true ),
				'low_stock_amount'  => get_post_meta( $id, '_low_stock_amount', true ),
				'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ),
				'virtual'           => get_post_meta( $id, '_virtual', true ),
				'downloadable'      => get_post_meta( $id, '_downloadable', true ),
				'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ),
				'download_limit'    => get_post_meta( $id, '_download_limit', true ),
				'download_expiry'   => get_post_meta( $id, '_download_expiry', true ),
				'image_id'          => get_post_thumbnail_id( $id ),
				'backorders'        => get_post_meta( $id, '_backorders', true ),
				'sku'               => get_post_meta( $id, '_sku', true ),
				'stock_quantity'    => get_post_meta( $id, '_stock', true ),
				'weight'            => get_post_meta( $id, '_weight', true ),
				'length'            => get_post_meta( $id, '_length', true ),
				'width'             => get_post_meta( $id, '_width', true ),
				'height'            => get_post_meta( $id, '_height', true ),
				'tax_class'         => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ),
			)
		);

		if ( $product->is_on_sale( 'edit' ) ) {
			$product->set_price( $product->get_sale_price( 'edit' ) );
		} else {
			$product->set_price( $product->get_regular_price( 'edit' ) );
		}

		$parent_object   = get_post( $product->get_parent_id() );
		$terms           = get_the_terms( $product->get_parent_id(), 'product_visibility' );
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );

		if ( $exclude_search && $exclude_catalog ) {
			$catalog_visibility = 'hidden';
		} elseif ( $exclude_search ) {
			$catalog_visibility = 'catalog';
		} elseif ( $exclude_catalog ) {
			$catalog_visibility = 'search';
		} else {
			$catalog_visibility = 'visible';
		}

		$product->set_parent_data(
			array(
				'title'              => $parent_object ? $parent_object->post_title : '',
				'status'             => $parent_object ? $parent_object->post_status : '',
				'sku'                => get_post_meta( $product->get_parent_id(), '_sku', true ),
				'manage_stock'       => get_post_meta( $product->get_parent_id(), '_manage_stock', true ),
				'backorders'         => get_post_meta( $product->get_parent_id(), '_backorders', true ),
				'stock_quantity'     => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ),
				'weight'             => get_post_meta( $product->get_parent_id(), '_weight', true ),
				'length'             => get_post_meta( $product->get_parent_id(), '_length', true ),
				'width'              => get_post_meta( $product->get_parent_id(), '_width', true ),
				'height'             => get_post_meta( $product->get_parent_id(), '_height', true ),
				'tax_class'          => get_post_meta( $product->get_parent_id(), '_tax_class', true ),
				'shipping_class_id'  => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ),
				'image_id'           => get_post_thumbnail_id( $product->get_parent_id() ),
				'purchase_note'      => get_post_meta( $product->get_parent_id(), '_purchase_note', true ),
				'catalog_visibility' => $catalog_visibility,
			)
		);

		// Pull data from the parent when there is no user-facing way to set props.
		$product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) );
		$product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) );
		$product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) );
	}

	/**
	 * For all stored terms in all taxonomies, save them to the DB.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_terms( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
		}
	}

	/**
	 * Update visibility terms based on props.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_visibility( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) {
			$terms = array();

			if ( 'outofstock' === $product->get_stock_status() ) {
				$terms[] = 'outofstock';
			}

			wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false );
		}
	}

	/**
	 * Update attribute meta values.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_attributes( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'attributes', $changes ) ) {
			global $wpdb;

			$product_id             = $product->get_id();
			$attributes             = $product->get_attributes();
			$updated_attribute_keys = array();
			foreach ( $attributes as $key => $value ) {
				update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
				$updated_attribute_keys[] = 'attribute_' . $key;
			}

			// Remove old taxonomies attributes so data is kept up to date - first get attribute key names.
			$delete_attribute_keys = $wpdb->get_col(
				$wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
					"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
					$wpdb->esc_like( 'attribute_' ) . '%',
					$product_id
				)
			);

			foreach ( $delete_attribute_keys as $key ) {
				delete_post_meta( $product_id, $key );
			}
		}
	}

	/**
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	public function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_variation_description' => 'description',
		);

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value   = $product->{"get_$prop"}( 'edit' );
			$updated = update_post_meta( $product->get_id(), $meta_key, $value );
			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		parent::update_post_meta( $product, $force );
	}

	/**
	 * Update product variation guid.
	 *
	 * @param WC_Product_Variation $product Product variation object.
	 *
	 * @since 3.6.0
	 */
	protected function update_guid( $product ) {
		global $wpdb;

		$guid = home_url(
			add_query_arg(
				array(
					'post_type' => 'product_variation',
					'p'         => $product->get_id(),
				),
				''
			)
		);
		$wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) );
	}
}
class-wc-shipping-zone-data-store.php000064400000026274151542600450013637 0ustar00<?php
/**
 * Class WC_Shipping_Zone_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Shipping Zone Data Store.
 *
 * @version  3.0.0
 */
class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Shipping_Zone_Data_Store_Interface {

	/**
	 * Method to create a new shipping zone.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	public function create( &$zone ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_shipping_zones',
			array(
				'zone_name'  => $zone->get_zone_name(),
				'zone_order' => $zone->get_zone_order(),
			)
		);
		$zone->set_id( $wpdb->insert_id );
		$zone->save_meta_data();
		$this->save_locations( $zone );
		$zone->apply_changes();
		WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );
	}

	/**
	 * Update zone in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	public function update( &$zone ) {
		global $wpdb;
		if ( $zone->get_id() ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_shipping_zones',
				array(
					'zone_name'  => $zone->get_zone_name(),
					'zone_order' => $zone->get_zone_order(),
				),
				array( 'zone_id' => $zone->get_id() )
			);
		}
		$zone->save_meta_data();
		$this->save_locations( $zone );
		$zone->apply_changes();
		WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );
	}

	/**
	 * Method to read a shipping zone from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 * @throws Exception If invalid data store.
	 */
	public function read( &$zone ) {
		global $wpdb;

		// Zone 0 is used as a default if no other zones fit.
		if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) {
			$this->read_zone_locations( $zone );
			$zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) );
			$zone->read_meta_data();
			$zone->set_object_read( true );

			/**
			 * Indicate that the WooCommerce shipping zone has been loaded.
			 *
			 * @param WC_Shipping_Zone $zone The shipping zone that has been loaded.
			 */
			do_action( 'woocommerce_shipping_zone_loaded', $zone );
			return;
		}

		$zone_data = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1",
				$zone->get_id()
			)
		);

		if ( ! $zone_data ) {
			throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
		}

		$zone->set_zone_name( $zone_data->zone_name );
		$zone->set_zone_order( $zone_data->zone_order );
		$this->read_zone_locations( $zone );
		$zone->read_meta_data();
		$zone->set_object_read( true );

		/** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */
		do_action( 'woocommerce_shipping_zone_loaded', $zone );
	}

	/**
	 * Deletes a shipping zone from the database.
	 *
	 * @since  3.0.0
	 * @param  WC_Shipping_Zone $zone Shipping zone object.
	 * @param  array            $args Array of args to pass to the delete method.
	 * @return void
	 */
	public function delete( &$zone, $args = array() ) {
		$zone_id = $zone->get_id();

		if ( $zone_id ) {
			global $wpdb;

			// Delete methods and their settings.
			$methods = $this->get_methods( $zone_id, false );

			if ( $methods ) {
				foreach ( $methods as $method ) {
					$this->delete_method( $method->instance_id );
				}
			}

			// Delete zone.
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) );

			$zone->set_id( null );

			WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
			WC_Cache_Helper::get_transient_version( 'shipping', true );

			do_action( 'woocommerce_delete_shipping_zone', $zone_id );
		}
	}

	/**
	 * Get a list of shipping methods for a specific zone.
	 *
	 * @since  3.0.0
	 * @param  int  $zone_id      Zone ID.
	 * @param  bool $enabled_only True to request enabled methods only.
	 * @return array               Array of objects containing method_id, method_order, instance_id, is_enabled
	 */
	public function get_methods( $zone_id, $enabled_only ) {
		global $wpdb;

		if ( $enabled_only ) {
			$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1";
		} else {
			$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d";
		}

		return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Get count of methods for a zone.
	 *
	 * @since  3.0.0
	 * @param  int $zone_id Zone ID.
	 * @return int Method Count
	 */
	public function get_method_count( $zone_id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) );
	}

	/**
	 * Add a shipping method to a zone.
	 *
	 * @since  3.0.0
	 * @param  int    $zone_id Zone ID.
	 * @param  string $type    Method Type/ID.
	 * @param  int    $order   Method Order.
	 * @return int             Instance ID
	 */
	public function add_method( $zone_id, $type, $order ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_shipping_zone_methods',
			array(
				'method_id'    => $type,
				'zone_id'      => $zone_id,
				'method_order' => $order,
			),
			array(
				'%s',
				'%d',
				'%d',
			)
		);
		return $wpdb->insert_id;
	}

	/**
	 * Delete a method instance.
	 *
	 * @since 3.0.0
	 * @param int $instance_id Instance ID.
	 */
	public function delete_method( $instance_id ) {
		global $wpdb;

		$method = $this->get_method( $instance_id );

		if ( ! $method ) {
			return;
		}

		delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' );

		$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) );

		do_action( 'woocommerce_delete_shipping_zone_method', $instance_id );
	}

	/**
	 * Get a shipping zone method instance.
	 *
	 * @since  3.0.0
	 * @param  int $instance_id Instance ID.
	 * @return object
	 */
	public function get_method( $instance_id ) {
		global $wpdb;
		return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) );
	}

	/**
	 * Find a matching zone ID for a given package.
	 *
	 * @since  3.0.0
	 * @param  object $package Package information.
	 * @return int
	 */
	public function get_zone_id_from_package( $package ) {
		global $wpdb;

		$country   = strtoupper( wc_clean( $package['destination']['country'] ) );
		$state     = strtoupper( wc_clean( $package['destination']['state'] ) );
		$continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) );
		$postcode  = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) );

		// Work out criteria for our zone search.
		$criteria   = array();
		$criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country );
		$criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state );
		$criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent );
		$criteria[] = 'OR ( location_type IS NULL ) )';

		// Postcode range and wildcard matching.
		$postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" );

		if ( $postcode_locations ) {
			$zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) );
			$matches                      = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country );
			$do_not_match                 = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) );

			if ( ! empty( $do_not_match ) ) {
				$criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')';
			}
		}

		/**
		 * Get shipping zone criteria
		 *
		 * @since 3.6.6
		 * @param array $criteria Get zone criteria.
		 * @param array $package Package information.
		 * @param array $postcode_locations Postcode range and wildcard matching.
		 */
		$criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations );

		// Get matching zones.
		return $wpdb->get_var(
			"SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode'
			WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			. ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1'
		);
	}

	/**
	 * Return an ordered list of zones.
	 *
	 * @since 3.0.0
	 * @return array An array of objects containing a zone_id, zone_name, and zone_order.
	 */
	public function get_zones() {
		global $wpdb;
		return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" );
	}


	/**
	 * Return a zone ID from an instance ID.
	 *
	 * @since  3.0.0
	 * @param  int $id Instance ID.
	 * @return int
	 */
	public function get_zone_id_by_instance_id( $id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) );
	}

	/**
	 * Read location data from the database.
	 *
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	private function read_zone_locations( &$zone ) {
		global $wpdb;

		$locations = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d",
				$zone->get_id()
			)
		);

		if ( $locations ) {
			foreach ( $locations as $location ) {
				$zone->add_location( $location->location_code, $location->location_type );
			}
		}
	}

	/**
	 * Save locations to the DB.
	 * This function clears old locations, then re-inserts new if any changes are found.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 *
	 * @return bool|void
	 */
	private function save_locations( &$zone ) {
		$changed_props = array_keys( $zone->get_changes() );
		if ( ! in_array( 'zone_locations', $changed_props, true ) ) {
			return false;
		}

		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) );

		foreach ( $zone->get_zone_locations( 'edit' ) as $location ) {
			$wpdb->insert(
				$wpdb->prefix . 'woocommerce_shipping_zone_locations',
				array(
					'zone_id'       => $zone->get_id(),
					'location_code' => $location->code,
					'location_type' => $location->type,
				)
			);
		}
	}
}
class-wc-webhook-data-store.php000064400000034611151542600450012475 0ustar00<?php
/**
 * Webhook Data Store
 *
 * @version  3.3.0
 * @package  WooCommerce\Classes\Data_Store
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Webhook data store class.
 */
class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {

	/**
	 * Create a new webhook in the database.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook Webhook instance.
	 */
	public function create( &$webhook ) {
		global $wpdb;

		$changes = $webhook->get_changes();
		if ( isset( $changes['date_created'] ) ) {
			$date_created     = $webhook->get_date_created()->date( 'Y-m-d H:i:s' );
			$date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() );
		} else {
			$date_created     = current_time( 'mysql' );
			$date_created_gmt = current_time( 'mysql', 1 );
			$webhook->set_date_created( $date_created );
		}

		// Pending delivery by default if not set while creating a new webhook.
		if ( ! isset( $changes['pending_delivery'] ) ) {
			$webhook->set_pending_delivery( true );
		}

		$data = array(
			'status'           => $webhook->get_status( 'edit' ),
			'name'             => $webhook->get_name( 'edit' ),
			'user_id'          => $webhook->get_user_id( 'edit' ),
			'delivery_url'     => $webhook->get_delivery_url( 'edit' ),
			'secret'           => $webhook->get_secret( 'edit' ),
			'topic'            => $webhook->get_topic( 'edit' ),
			'date_created'     => $date_created,
			'date_created_gmt' => $date_created_gmt,
			'api_version'      => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
			'failure_count'    => $webhook->get_failure_count( 'edit' ),
			'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
		);

		$wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok.

		$webhook_id = $wpdb->insert_id;
		$webhook->set_id( $webhook_id );
		$webhook->apply_changes();

		$this->delete_transients( $webhook->get_status( 'edit' ) );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
		do_action( 'woocommerce_new_webhook', $webhook_id, $webhook );
	}

	/**
	 * Read a webhook from the database.
	 *
	 * @since  3.3.0
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @throws Exception When webhook is invalid.
	 */
	public function read( &$webhook ) {
		global $wpdb;

		$data = wp_cache_get( $webhook->get_id(), 'webhooks' );

		if ( false === $data ) {
			$data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok.

			wp_cache_add( $webhook->get_id(), $data, 'webhooks' );
		}

		if ( is_array( $data ) ) {
			$webhook->set_props(
				array(
					'id'               => $data['webhook_id'],
					'status'           => $data['status'],
					'name'             => $data['name'],
					'user_id'          => $data['user_id'],
					'delivery_url'     => $data['delivery_url'],
					'secret'           => $data['secret'],
					'topic'            => $data['topic'],
					'date_created'     => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'],
					'date_modified'    => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'],
					'api_version'      => $data['api_version'],
					'failure_count'    => $data['failure_count'],
					'pending_delivery' => $data['pending_delivery'],
				)
			);
			$webhook->set_object_read( true );

			do_action( 'woocommerce_webhook_loaded', $webhook );
		} else {
			throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) );
		}
	}

	/**
	 * Update a webhook.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook Webhook instance.
	 */
	public function update( &$webhook ) {
		global $wpdb;

		$changes = $webhook->get_changes();
		$trigger = isset( $changes['delivery_url'] );

		if ( isset( $changes['date_modified'] ) ) {
			$date_modified     = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' );
			$date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() );
		} else {
			$date_modified     = current_time( 'mysql' );
			$date_modified_gmt = current_time( 'mysql', 1 );
			$webhook->set_date_modified( $date_modified );
		}

		$data = array(
			'status'            => $webhook->get_status( 'edit' ),
			'name'              => $webhook->get_name( 'edit' ),
			'user_id'           => $webhook->get_user_id( 'edit' ),
			'delivery_url'      => $webhook->get_delivery_url( 'edit' ),
			'secret'            => $webhook->get_secret( 'edit' ),
			'topic'             => $webhook->get_topic( 'edit' ),
			'date_modified'     => $date_modified,
			'date_modified_gmt' => $date_modified_gmt,
			'api_version'       => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
			'failure_count'     => $webhook->get_failure_count( 'edit' ),
			'pending_delivery'  => $webhook->get_pending_delivery( 'edit' ),
		);

		$wpdb->update(
			$wpdb->prefix . 'wc_webhooks',
			$data,
			array(
				'webhook_id' => $webhook->get_id(),
			)
		); // WPCS: DB call ok.

		$webhook->apply_changes();

		if ( isset( $changes['status'] ) ) {
			// We need to delete all transients, because we can't be sure of the old status.
			$this->delete_transients( 'all' );
		}
		wp_cache_delete( $webhook->get_id(), 'webhooks' );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );

		if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) {
			$webhook->deliver_ping();
		}

		do_action( 'woocommerce_webhook_updated', $webhook->get_id() );
	}

	/**
	 * Remove a webhook from the database.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook      Webhook instance.
	 */
	public function delete( &$webhook ) {
		global $wpdb;

		$wpdb->delete(
			$wpdb->prefix . 'wc_webhooks',
			array(
				'webhook_id' => $webhook->get_id(),
			),
			array( '%d' )
		); // WPCS: cache ok, DB call ok.

		$this->delete_transients( 'all' );
		wp_cache_delete( $webhook->get_id(), 'webhooks' );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
		do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook );
	}

	/**
	 * Get API version number.
	 *
	 * @since  3.3.0
	 * @param  string $api_version REST API version.
	 * @return int
	 */
	public function get_api_version_number( $api_version ) {
		return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) );
	}

	/**
	 * Get webhooks IDs from the database.
	 *
	 * @since  3.3.0
	 * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys.
	 * @param  string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0.
	 * @return int[]
	 */
	public function get_webhooks_ids( $status = '' ) {
		if ( ! empty( $status ) ) {
			$this->validate_status( $status );
		}

		$ids = get_transient( $this->get_transient_key( $status ) );

		if ( false === $ids ) {
			$ids = $this->search_webhooks(
				array(
					'limit'  => -1,
					'status' => $status,
				)
			);
			$ids = array_map( 'absint', $ids );
			set_transient( $this->get_transient_key( $status ), $ids );
		}

		return $ids;
	}

	/**
	 * Search webhooks.
	 *
	 * @param  array $args Search arguments.
	 * @return array|object
	 */
	public function search_webhooks( $args ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'limit'    => 10,
				'offset'   => 0,
				'order'    => 'DESC',
				'orderby'  => 'id',
				'paginate' => false,
			)
		);

		// Map post statuses.
		$statuses = array(
			'publish' => 'active',
			'draft'   => 'paused',
			'pending' => 'disabled',
		);

		// Map orderby to support a few post keys.
		$orderby_mapping = array(
			'ID'            => 'webhook_id',
			'id'            => 'webhook_id',
			'name'          => 'name',
			'title'         => 'name',
			'post_title'    => 'name',
			'post_name'     => 'name',
			'date_created'  => 'date_created_gmt',
			'date'          => 'date_created_gmt',
			'post_date'     => 'date_created_gmt',
			'date_modified' => 'date_modified_gmt',
			'modified'      => 'date_modified_gmt',
			'post_modified' => 'date_modified_gmt',
		);
		$orderby         = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id';
		$sort            = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
		$order           = "ORDER BY {$orderby} {$sort}";
		$limit           = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
		$offset          = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
		$status          = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : '';
		$search          = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : '';
		$include         = '';
		$exclude         = '';
		$date_created    = '';
		$date_modified   = '';
		$user_id         = '';

		if ( ! empty( $args['include'] ) ) {
			$args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) );
			$include         = 'AND webhook_id IN (' . $args['include'] . ')';
		}

		if ( ! empty( $args['exclude'] ) ) {
			$args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) );
			$exclude         = 'AND webhook_id NOT IN (' . $args['exclude'] . ')';
		}

		if ( ! empty( $args['user_id'] ) ) {
			$user_id = $wpdb->prepare( 'AND `user_id` = %d', absint( $args['user_id'] ) );
		}

		if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
			$args['after']  = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
			$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];

			$date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
		}

		if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
			$args['modified_after']  = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
			$args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];

			$date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
		}

		// Check for cache.
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) );
		$cache_value = wp_cache_get( $cache_key, 'webhook_search_results' );

		if ( $cache_value ) {
			return $cache_value;
		}

		if ( $args['paginate'] ) {
			$query = trim(
				"SELECT SQL_CALC_FOUND_ROWS webhook_id
				FROM {$wpdb->prefix}wc_webhooks
				WHERE 1=1
				{$status}
				{$search}
				{$include}
				{$exclude}
				{$date_created}
				{$date_modified}
				{$user_id}
				{$order}
				{$limit}
				{$offset}"
			);

			$webhook_ids  = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$total        = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' );
			$return_value = (object) array(
				'webhooks'      => $webhook_ids,
				'total'         => $total,
				'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1,
			);
		} else {
			$query = trim(
				"SELECT webhook_id
				FROM {$wpdb->prefix}wc_webhooks
				WHERE 1=1
				{$status}
				{$search}
				{$include}
				{$exclude}
				{$date_created}
				{$date_modified}
				{$user_id}
				{$order}
				{$limit}
				{$offset}"
			);

			$webhook_ids  = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$return_value = $webhook_ids;
		}

		wp_cache_set( $cache_key, $return_value, 'webhook_search_results' );

		return $return_value;
	}

	/**
	 * Count webhooks.
	 *
	 * @since 3.6.0
	 * @param string $status Status to count.
	 * @return int
	 */
	protected function get_webhook_count( $status = 'active' ) {
		global $wpdb;
		$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count';
		$count     = wp_cache_get( $cache_key, 'webhooks' );

		if ( false === $count ) {
			$count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) );

			wp_cache_add( $cache_key, $count, 'webhooks' );
		}

		return $count;
	}

	/**
	 * Get total webhook counts by status.
	 *
	 * @return array
	 */
	public function get_count_webhooks_by_status() {
		$statuses = array_keys( wc_get_webhook_statuses() );
		$counts   = array();

		foreach ( $statuses as $status ) {
			$counts[ $status ] = $this->get_webhook_count( $status );
		}

		return $counts;
	}

	/**
	 * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses().
	 *
	 * @since  3.6.0
	 * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys.
	 * @param  string $status Status to check.
	 */
	private function validate_status( $status ) {
		if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
			throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) );
		}
	}

	/**
	 * Get the transient key used to cache a set of webhook IDs, optionally filtered by status.
	 *
	 * @since  3.6.0
	 * @param  string $status Optional - status of cache key.
	 * @return string
	 */
	private function get_transient_key( $status = '' ) {
		return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status );
	}

	/**
	 * Delete the transients used to cache a set of webhook IDs, optionally filtered by status.
	 *
	 * @since 3.6.0
	 * @param string $status Optional - status of cache to delete, or 'all' to delete all caches.
	 */
	private function delete_transients( $status = '' ) {

		// Always delete the non-filtered cache.
		delete_transient( $this->get_transient_key( '' ) );

		if ( ! empty( $status ) ) {
			if ( 'all' === $status ) {
				foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) {
					delete_transient( $this->get_transient_key( $status_key ) );
				}
			} else {
				delete_transient( $this->get_transient_key( $status ) );
			}
		}
	}
}
ActionScheduler_DBLogger.php000064400000010620151547307000012037 0ustar00<?php

/**
 * Class ActionScheduler_DBLogger
 *
 * Action logs data table data store.
 *
 * @since 3.0.0
 */
class ActionScheduler_DBLogger extends ActionScheduler_Logger {

	/**
	 * Add a record to an action log.
	 *
	 * @param int           $action_id Action ID.
	 * @param string        $message Message to be saved in the log entry.
	 * @param DateTime|null $date Timestamp of the log entry.
	 *
	 * @return int     The log entry ID.
	 */
	public function log( $action_id, $message, ?DateTime $date = null ) {
		if ( empty( $date ) ) {
			$date = as_get_datetime_object();
		} else {
			$date = clone $date;
		}

		$date_gmt = $date->format( 'Y-m-d H:i:s' );
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
		$date_local = $date->format( 'Y-m-d H:i:s' );

		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
		global $wpdb;
		$wpdb->insert(
			$wpdb->actionscheduler_logs,
			array(
				'action_id'      => $action_id,
				'message'        => $message,
				'log_date_gmt'   => $date_gmt,
				'log_date_local' => $date_local,
			),
			array( '%d', '%s', '%s', '%s' )
		);

		return $wpdb->insert_id;
	}

	/**
	 * Retrieve an action log entry.
	 *
	 * @param int $entry_id Log entry ID.
	 *
	 * @return ActionScheduler_LogEntry
	 */
	public function get_entry( $entry_id ) {
		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
		global $wpdb;
		$entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) );

		return $this->create_entry_from_db_record( $entry );
	}

	/**
	 * Create an action log entry from a database record.
	 *
	 * @param object $record Log entry database record object.
	 *
	 * @return ActionScheduler_LogEntry
	 */
	private function create_entry_from_db_record( $record ) {
		if ( empty( $record ) ) {
			return new ActionScheduler_NullLogEntry();
		}

		if ( is_null( $record->log_date_gmt ) ) {
			$date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE );
		} else {
			$date = as_get_datetime_object( $record->log_date_gmt );
		}

		return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date );
	}

	/**
	 * Retrieve an action's log entries from the database.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return ActionScheduler_LogEntry[]
	 */
	public function get_logs( $action_id ) {
		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
		global $wpdb;

		$records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) );

		return array_map( array( $this, 'create_entry_from_db_record' ), $records );
	}

	/**
	 * Initialize the data store.
	 *
	 * @codeCoverageIgnore
	 */
	public function init() {
		$table_maker = new ActionScheduler_LoggerSchema();
		$table_maker->init();
		$table_maker->register_tables();

		parent::init();

		add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 );
	}

	/**
	 * Delete the action logs for an action.
	 *
	 * @param int $action_id Action ID.
	 */
	public function clear_deleted_action_logs( $action_id ) {
		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
		global $wpdb;
		$wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) );
	}

	/**
	 * Bulk add cancel action log entries.
	 *
	 * @param array $action_ids List of action ID.
	 */
	public function bulk_log_cancel_actions( $action_ids ) {
		if ( empty( $action_ids ) ) {
			return;
		}

		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
		global $wpdb;
		$date     = as_get_datetime_object();
		$date_gmt = $date->format( 'Y-m-d H:i:s' );
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
		$date_local = $date->format( 'Y-m-d H:i:s' );
		$message    = __( 'action canceled', 'action-scheduler' );
		$format     = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')';
		$sql_query  = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES ";
		$value_rows = array();

		foreach ( $action_ids as $action_id ) {
			$value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}
		$sql_query .= implode( ',', $value_rows );

		$wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}
}
ActionScheduler_DBStore.php000064400000114765151547307000011733 0ustar00<?php

/**
 * Class ActionScheduler_DBStore
 *
 * Action data table data store.
 *
 * @since 3.0.0
 */
class ActionScheduler_DBStore extends ActionScheduler_Store {

	/**
	 * Used to share information about the before_date property of claims internally.
	 *
	 * This is used in preference to passing the same information as a method param
	 * for backwards-compatibility reasons.
	 *
	 * @var DateTime|null
	 */
	private $claim_before_date = null;

	/**
	 * Maximum length of args.
	 *
	 * @var int
	 */
	protected static $max_args_length = 8000;

	/**
	 * Maximum length of index.
	 *
	 * @var int
	 */
	protected static $max_index_length = 191;

	/**
	 * List of claim filters.
	 *
	 * @var array
	 */
	protected $claim_filters = array(
		'group'          => '',
		'hooks'          => '',
		'exclude-groups' => '',
	);

	/**
	 * Initialize the data store
	 *
	 * @codeCoverageIgnore
	 */
	public function init() {
		$table_maker = new ActionScheduler_StoreSchema();
		$table_maker->init();
		$table_maker->register_tables();
	}

	/**
	 * Save an action, checks if this is a unique action before actually saving.
	 *
	 * @param ActionScheduler_Action $action         Action object.
	 * @param DateTime|null          $scheduled_date Optional schedule date. Default null.
	 *
	 * @return int                  Action ID.
	 * @throws RuntimeException     Throws exception when saving the action fails.
	 */
	public function save_unique_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		return $this->save_action_to_db( $action, $scheduled_date, true );
	}

	/**
	 * Save an action. Can save duplicate action as well, prefer using `save_unique_action` instead.
	 *
	 * @param ActionScheduler_Action $action Action object.
	 * @param DateTime|null          $scheduled_date Optional schedule date. Default null.
	 *
	 * @return int Action ID.
	 * @throws RuntimeException     Throws exception when saving the action fails.
	 */
	public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		return $this->save_action_to_db( $action, $scheduled_date, false );
	}

	/**
	 * Save an action.
	 *
	 * @param ActionScheduler_Action $action Action object.
	 * @param ?DateTime              $date Optional schedule date. Default null.
	 * @param bool                   $unique Whether the action should be unique.
	 *
	 * @return int Action ID.
	 * @throws \RuntimeException     Throws exception when saving the action fails.
	 */
	private function save_action_to_db( ActionScheduler_Action $action, ?DateTime $date = null, $unique = false ) {
		global $wpdb;

		try {
			$this->validate_action( $action );

			$data = array(
				'hook'                 => $action->get_hook(),
				'status'               => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ),
				'scheduled_date_gmt'   => $this->get_scheduled_date_string( $action, $date ),
				'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
				'schedule'             => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
				'group_id'             => current( $this->get_group_ids( $action->get_group() ) ),
				'priority'             => $action->get_priority(),
			);

			$args = wp_json_encode( $action->get_args() );
			if ( strlen( $args ) <= static::$max_index_length ) {
				$data['args'] = $args;
			} else {
				$data['args']          = $this->hash_args( $args );
				$data['extended_args'] = $args;
			}

			$insert_sql = $this->build_insert_sql( $data, $unique );

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_sql should be already prepared.
			$wpdb->query( $insert_sql );
			$action_id = $wpdb->insert_id;

			if ( is_wp_error( $action_id ) ) {
				throw new \RuntimeException( $action_id->get_error_message() );
			} elseif ( empty( $action_id ) ) {
				if ( $unique ) {
					return 0;
				}
				throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) );
			}

			do_action( 'action_scheduler_stored_action', $action_id );

			return $action_id;
		} catch ( \Exception $e ) {
			/* translators: %s: error message */
			throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
		}
	}

	/**
	 * Helper function to build insert query.
	 *
	 * @param array $data Row data for action.
	 * @param bool  $unique Whether the action should be unique.
	 *
	 * @return string Insert query.
	 */
	private function build_insert_sql( array $data, $unique ) {
		global $wpdb;

		$columns      = array_keys( $data );
		$values       = array_values( $data );
		$placeholders = array_map( array( $this, 'get_placeholder_for_column' ), $columns );

		$table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions';

		$column_sql      = '`' . implode( '`, `', $columns ) . '`';
		$placeholder_sql = implode( ', ', $placeholders );
		$where_clause    = $this->build_where_clause_for_insert( $data, $table_name, $unique );

		// 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.
		$insert_query = $wpdb->prepare(
			"
INSERT INTO $table_name ( $column_sql )
SELECT $placeholder_sql FROM DUAL
WHERE ( $where_clause ) IS NULL",
			$values
		);
		// phpcs:enable

		return $insert_query;
	}

	/**
	 * Helper method to build where clause for action insert statement.
	 *
	 * @param array  $data Row data for action.
	 * @param string $table_name Action table name.
	 * @param bool   $unique Where action should be unique.
	 *
	 * @return string Where clause to be used with insert.
	 */
	private function build_where_clause_for_insert( $data, $table_name, $unique ) {
		global $wpdb;

		if ( ! $unique ) {
			return 'SELECT NULL FROM DUAL';
		}

		$pending_statuses            = array(
			ActionScheduler_Store::STATUS_PENDING,
			ActionScheduler_Store::STATUS_RUNNING,
		);
		$pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $pending_status_placeholders is hardcoded.
		$where_clause = $wpdb->prepare(
			"
SELECT action_id FROM $table_name
WHERE status IN ( $pending_status_placeholders )
AND hook = %s
AND `group_id` = %d
",
			array_merge(
				$pending_statuses,
				array(
					$data['hook'],
					$data['group_id'],
				)
			)
		);
		// phpcs:enable

		return "$where_clause" . ' LIMIT 1';
	}

	/**
	 * Helper method to get $wpdb->prepare placeholder for a given column name.
	 *
	 * @param string $column_name Name of column in actions table.
	 *
	 * @return string Placeholder to use for given column.
	 */
	private function get_placeholder_for_column( $column_name ) {
		$string_columns = array(
			'hook',
			'status',
			'scheduled_date_gmt',
			'scheduled_date_local',
			'args',
			'schedule',
			'last_attempt_gmt',
			'last_attempt_local',
			'extended_args',
		);

		return in_array( $column_name, $string_columns, true ) ? '%s' : '%d';
	}

	/**
	 * Generate a hash from json_encoded $args using MD5 as this isn't for security.
	 *
	 * @param string $args JSON encoded action args.
	 * @return string
	 */
	protected function hash_args( $args ) {
		return md5( $args );
	}

	/**
	 * Get action args query param value from action args.
	 *
	 * @param array $args Action args.
	 * @return string
	 */
	protected function get_args_for_query( $args ) {
		$encoded = wp_json_encode( $args );
		if ( strlen( $encoded ) <= static::$max_index_length ) {
			return $encoded;
		}
		return $this->hash_args( $encoded );
	}
	/**
	 * Get a group's ID based on its name/slug.
	 *
	 * @param string|array $slugs                The string name of a group, or names for several groups.
	 * @param bool         $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
	 *
	 * @return array The group IDs, if they exist or were successfully created. May be empty.
	 */
	protected function get_group_ids( $slugs, $create_if_not_exists = true ) {
		$slugs     = (array) $slugs;
		$group_ids = array();

		if ( empty( $slugs ) ) {
			return array();
		}

		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		foreach ( $slugs as $slug ) {
			$group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );

			if ( empty( $group_id ) && $create_if_not_exists ) {
				$group_id = $this->create_group( $slug );
			}

			if ( $group_id ) {
				$group_ids[] = $group_id;
			}
		}

		return $group_ids;
	}

	/**
	 * Create an action group.
	 *
	 * @param string $slug Group slug.
	 *
	 * @return int Group ID.
	 */
	protected function create_group( $slug ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) );

		return (int) $wpdb->insert_id;
	}

	/**
	 * Retrieve an action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return ActionScheduler_Action
	 */
	public function fetch_action( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$data = $wpdb->get_row(
			$wpdb->prepare(
				"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",
				$action_id
			)
		);

		if ( empty( $data ) ) {
			return $this->get_null_action();
		}

		if ( ! empty( $data->extended_args ) ) {
			$data->args = $data->extended_args;
			unset( $data->extended_args );
		}

		// Convert NULL dates to zero dates.
		$date_fields = array(
			'scheduled_date_gmt',
			'scheduled_date_local',
			'last_attempt_gmt',
			'last_attempt_gmt',
		);
		foreach ( $date_fields as $date_field ) {
			if ( is_null( $data->$date_field ) ) {
				$data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE;
			}
		}

		try {
			$action = $this->make_action_from_db_record( $data );
		} catch ( ActionScheduler_InvalidActionException $exception ) {
			do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception );
			return $this->get_null_action();
		}

		return $action;
	}

	/**
	 * Create a null action.
	 *
	 * @return ActionScheduler_NullAction
	 */
	protected function get_null_action() {
		return new ActionScheduler_NullAction();
	}

	/**
	 * Create an action from a database record.
	 *
	 * @param object $data Action database record.
	 *
	 * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction
	 */
	protected function make_action_from_db_record( $data ) {

		$hook     = $data->hook;
		$args     = json_decode( $data->args, true );
		$schedule = unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize

		$this->validate_args( $args, $data->action_id );
		$this->validate_schedule( $schedule, $data->action_id );

		if ( empty( $schedule ) ) {
			$schedule = new ActionScheduler_NullSchedule();
		}
		$group = $data->group ? $data->group : '';

		return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority );
	}

	/**
	 * Returns the SQL statement to query (or count) actions.
	 *
	 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
	 *
	 * @param array  $query Filtering options.
	 * @param string $select_or_count  Whether the SQL should select and return the IDs or just the row count.
	 *
	 * @return string SQL statement already properly escaped.
	 * @throws \InvalidArgumentException If the query is invalid.
	 * @throws \RuntimeException When "unknown partial args matching value".
	 */
	protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {

		if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
			throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) );
		}

		$query = wp_parse_args(
			$query,
			array(
				'hook'                  => '',
				'args'                  => null,
				'partial_args_matching' => 'off', // can be 'like' or 'json'.
				'date'                  => null,
				'date_compare'          => '<=',
				'modified'              => null,
				'modified_compare'      => '<=',
				'group'                 => '',
				'status'                => '',
				'claimed'               => null,
				'per_page'              => 5,
				'offset'                => 0,
				'orderby'               => 'date',
				'order'                 => 'ASC',
			)
		);

		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$db_server_info = is_callable( array( $wpdb, 'db_server_info' ) ) ? $wpdb->db_server_info() : $wpdb->db_version();
		if ( false !== strpos( $db_server_info, 'MariaDB' ) ) {
			$supports_json = version_compare(
				PHP_VERSION_ID >= 80016 ? $wpdb->db_version() : preg_replace( '/[^0-9.].*/', '', str_replace( '5.5.5-', '', $db_server_info ) ),
				'10.2',
				'>='
			);
		} else {
			$supports_json = version_compare( $wpdb->db_version(), '5.7', '>=' );
		}

		$sql        = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id';
		$sql       .= " FROM {$wpdb->actionscheduler_actions} a";
		$sql_params = array();

		if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) {
			$sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id";
		}

		$sql .= ' WHERE 1=1';

		if ( ! empty( $query['group'] ) ) {
			$sql         .= ' AND g.slug=%s';
			$sql_params[] = $query['group'];
		}

		if ( ! empty( $query['hook'] ) ) {
			$sql         .= ' AND a.hook=%s';
			$sql_params[] = $query['hook'];
		}

		if ( ! is_null( $query['args'] ) ) {
			switch ( $query['partial_args_matching'] ) {
				case 'json':
					if ( ! $supports_json ) {
						throw new \RuntimeException( __( 'JSON partial matching not supported in your environment. Please check your MySQL/MariaDB version.', 'action-scheduler' ) );
					}
					$supported_types = array(
						'integer' => '%d',
						'boolean' => '%s',
						'double'  => '%f',
						'string'  => '%s',
					);
					foreach ( $query['args'] as $key => $value ) {
						$value_type = gettype( $value );
						if ( 'boolean' === $value_type ) {
							$value = $value ? 'true' : 'false';
						}
						$placeholder = isset( $supported_types[ $value_type ] ) ? $supported_types[ $value_type ] : false;
						if ( ! $placeholder ) {
							throw new \RuntimeException(
								sprintf(
									/* translators: %s: provided value type */
									__( 'The value type for the JSON partial matching is not supported. Must be either integer, boolean, double or string. %s type provided.', 'action-scheduler' ),
									$value_type
								)
							);
						}
						$sql         .= ' AND JSON_EXTRACT(a.args, %s)=' . $placeholder;
						$sql_params[] = '$.' . $key;
						$sql_params[] = $value;
					}
					break;
				case 'like':
					foreach ( $query['args'] as $key => $value ) {
						$sql         .= ' AND a.args LIKE %s';
						$json_partial = $wpdb->esc_like( trim( wp_json_encode( array( $key => $value ) ), '{}' ) );
						$sql_params[] = "%{$json_partial}%";
					}
					break;
				case 'off':
					$sql         .= ' AND a.args=%s';
					$sql_params[] = $this->get_args_for_query( $query['args'] );
					break;
				default:
					throw new \RuntimeException( __( 'Unknown partial args matching value.', 'action-scheduler' ) );
			}
		}

		if ( $query['status'] ) {
			$statuses     = (array) $query['status'];
			$placeholders = array_fill( 0, count( $statuses ), '%s' );
			$sql         .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')';
			$sql_params   = array_merge( $sql_params, array_values( $statuses ) );
		}

		if ( $query['date'] instanceof \DateTime ) {
			$date = clone $query['date'];
			$date->setTimezone( new \DateTimeZone( 'UTC' ) );
			$date_string  = $date->format( 'Y-m-d H:i:s' );
			$comparator   = $this->validate_sql_comparator( $query['date_compare'] );
			$sql         .= " AND a.scheduled_date_gmt $comparator %s";
			$sql_params[] = $date_string;
		}

		if ( $query['modified'] instanceof \DateTime ) {
			$modified = clone $query['modified'];
			$modified->setTimezone( new \DateTimeZone( 'UTC' ) );
			$date_string  = $modified->format( 'Y-m-d H:i:s' );
			$comparator   = $this->validate_sql_comparator( $query['modified_compare'] );
			$sql         .= " AND a.last_attempt_gmt $comparator %s";
			$sql_params[] = $date_string;
		}

		if ( true === $query['claimed'] ) {
			$sql .= ' AND a.claim_id != 0';
		} elseif ( false === $query['claimed'] ) {
			$sql .= ' AND a.claim_id = 0';
		} elseif ( ! is_null( $query['claimed'] ) ) {
			$sql         .= ' AND a.claim_id = %d';
			$sql_params[] = $query['claimed'];
		}

		if ( ! empty( $query['search'] ) ) {
			$sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s';
			for ( $i = 0; $i < 3; $i++ ) {
				$sql_params[] = sprintf( '%%%s%%', $query['search'] );
			}

			$search_claim_id = (int) $query['search'];
			if ( $search_claim_id ) {
				$sql         .= ' OR a.claim_id = %d';
				$sql_params[] = $search_claim_id;
			}

			$sql .= ')';
		}

		if ( 'select' === $select_or_count ) {
			if ( 'ASC' === strtoupper( $query['order'] ) ) {
				$order = 'ASC';
			} else {
				$order = 'DESC';
			}
			switch ( $query['orderby'] ) {
				case 'hook':
					$sql .= " ORDER BY a.hook $order";
					break;
				case 'group':
					$sql .= " ORDER BY g.slug $order";
					break;
				case 'modified':
					$sql .= " ORDER BY a.last_attempt_gmt $order";
					break;
				case 'none':
					break;
				case 'action_id':
					$sql .= " ORDER BY a.action_id $order";
					break;
				case 'date':
				default:
					$sql .= " ORDER BY a.scheduled_date_gmt $order";
					break;
			}

			if ( $query['per_page'] > 0 ) {
				$sql         .= ' LIMIT %d, %d';
				$sql_params[] = $query['offset'];
				$sql_params[] = $query['per_page'];
			}
		}

		if ( ! empty( $sql_params ) ) {
			$sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		return $sql;
	}

	/**
	 * Query for action count or list of action IDs.
	 *
	 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
	 *
	 * @see ActionScheduler_Store::query_actions for $query arg usage.
	 *
	 * @param array  $query      Query filtering options.
	 * @param string $query_type Whether to select or count the results. Defaults to select.
	 *
	 * @return string|array|null The IDs of actions matching the query. Null on failure.
	 */
	public function query_actions( $query = array(), $query_type = 'select' ) {
		/**
		 * Global.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		$sql = $this->get_query_actions_sql( $query, $query_type );

		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
	}

	/**
	 * Get a count of all actions in the store, grouped by status.
	 *
	 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
	 */
	public function action_counts() {
		global $wpdb;

		$sql  = "SELECT a.status, count(a.status) as 'count'";
		$sql .= " FROM {$wpdb->actionscheduler_actions} a";
		$sql .= ' GROUP BY a.status';

		$actions_count_by_status = array();
		$action_stati_and_labels = $this->get_status_labels();

		foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			// Ignore any actions with invalid status.
			if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) {
				$actions_count_by_status[ $action_data->status ] = $action_data->count;
			}
		}

		return $actions_count_by_status;
	}

	/**
	 * Cancel an action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return void
	 * @throws \InvalidArgumentException If the action update failed.
	 */
	public function cancel_action( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$updated = $wpdb->update(
			$wpdb->actionscheduler_actions,
			array( 'status' => self::STATUS_CANCELED ),
			array( 'action_id' => $action_id ),
			array( '%s' ),
			array( '%d' )
		);
		if ( false === $updated ) {
			/* translators: %s: action ID */
			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 ) );
		}
		do_action( 'action_scheduler_canceled_action', $action_id );
	}

	/**
	 * Cancel pending actions by hook.
	 *
	 * @since 3.0.0
	 *
	 * @param string $hook Hook name.
	 *
	 * @return void
	 */
	public function cancel_actions_by_hook( $hook ) {
		$this->bulk_cancel_actions( array( 'hook' => $hook ) );
	}

	/**
	 * Cancel pending actions by group.
	 *
	 * @param string $group Group slug.
	 *
	 * @return void
	 */
	public function cancel_actions_by_group( $group ) {
		$this->bulk_cancel_actions( array( 'group' => $group ) );
	}

	/**
	 * Bulk cancel actions.
	 *
	 * @since 3.0.0
	 *
	 * @param array $query_args Query parameters.
	 */
	protected function bulk_cancel_actions( $query_args ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		if ( ! is_array( $query_args ) ) {
			return;
		}

		// Don't cancel actions that are already canceled.
		if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) {
			return;
		}

		$action_ids = true;
		$query_args = wp_parse_args(
			$query_args,
			array(
				'per_page' => 1000,
				'status'   => self::STATUS_PENDING,
				'orderby'  => 'none',
			)
		);

		while ( $action_ids ) {
			$action_ids = $this->query_actions( $query_args );
			if ( empty( $action_ids ) ) {
				break;
			}

			$format     = array_fill( 0, count( $action_ids ), '%d' );
			$query_in   = '(' . implode( ',', $format ) . ')';
			$parameters = $action_ids;
			array_unshift( $parameters, self::STATUS_CANCELED );

			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$parameters
				)
			);

			do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
		}
	}

	/**
	 * Delete an action.
	 *
	 * @param int $action_id Action ID.
	 * @throws \InvalidArgumentException If the action deletion failed.
	 */
	public function delete_action( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) );
		if ( empty( $deleted ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		do_action( 'action_scheduler_deleted_action', $action_id );
	}

	/**
	 * Get the schedule date for an action.
	 *
	 * @param string $action_id Action ID.
	 *
	 * @return \DateTime The local date the action is scheduled to run, or the date that it ran.
	 */
	public function get_date( $action_id ) {
		$date = $this->get_date_gmt( $action_id );
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
		return $date;
	}

	/**
	 * Get the GMT schedule date for an action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @throws \InvalidArgumentException If action cannot be identified.
	 * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran.
	 */
	protected function get_date_gmt( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) );
		if ( empty( $record ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		if ( self::STATUS_PENDING === $record->status ) {
			return as_get_datetime_object( $record->scheduled_date_gmt );
		} else {
			return as_get_datetime_object( $record->last_attempt_gmt );
		}
	}

	/**
	 * Stake a claim on actions.
	 *
	 * @param int           $max_actions Maximum number of action to include in claim.
	 * @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now.
	 * @param array         $hooks Hooks to filter for.
	 * @param string        $group Group to filter for.
	 *
	 * @return ActionScheduler_ActionClaim
	 */
	public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
		$claim_id = $this->generate_claim_id();

		$this->claim_before_date = $before_date;
		$this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
		$action_ids              = $this->find_actions_by_claim_id( $claim_id );
		$this->claim_before_date = null;

		return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
	}

	/**
	 * Generate a new action claim.
	 *
	 * @return int Claim ID.
	 */
	protected function generate_claim_id() {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$now = as_get_datetime_object();
		$wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) );

		return $wpdb->insert_id;
	}

	/**
	 * Set a claim filter.
	 *
	 * @param string $filter_name Claim filter name.
	 * @param mixed  $filter_values Values to filter.
	 * @return void
	 */
	public function set_claim_filter( $filter_name, $filter_values ) {
		if ( isset( $this->claim_filters[ $filter_name ] ) ) {
			$this->claim_filters[ $filter_name ] = $filter_values;
		}
	}

	/**
	 * Get the claim filter value.
	 *
	 * @param string $filter_name Claim filter name.
	 * @return mixed
	 */
	public function get_claim_filter( $filter_name ) {
		if ( isset( $this->claim_filters[ $filter_name ] ) ) {
			return $this->claim_filters[ $filter_name ];
		}

		return '';
	}

	/**
	 * Mark actions claimed.
	 *
	 * @param string        $claim_id Claim Id.
	 * @param int           $limit Number of action to include in claim.
	 * @param DateTime|null $before_date Should use UTC timezone.
	 * @param array         $hooks Hooks to filter for.
	 * @param string        $group Group to filter for.
	 *
	 * @return int The number of actions that were claimed.
	 * @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist.
	 * @throws \RuntimeException Throws RuntimeException if unable to claim action.
	 */
	protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$now  = as_get_datetime_object();
		$date = is_null( $before_date ) ? $now : clone $before_date;
		// can't use $wpdb->update() because of the <= condition.
		$update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
		$params = array(
			$claim_id,
			$now->format( 'Y-m-d H:i:s' ),
			current_time( 'mysql' ),
		);

		// Set claim filters.
		if ( ! empty( $hooks ) ) {
			$this->set_claim_filter( 'hooks', $hooks );
		} else {
			$hooks = $this->get_claim_filter( 'hooks' );
		}
		if ( ! empty( $group ) ) {
			$this->set_claim_filter( 'group', $group );
		} else {
			$group = $this->get_claim_filter( 'group' );
		}

		$where    = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
		$params[] = $date->format( 'Y-m-d H:i:s' );
		$params[] = self::STATUS_PENDING;

		if ( ! empty( $hooks ) ) {
			$placeholders = array_fill( 0, count( $hooks ), '%s' );
			$where       .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
			$params       = array_merge( $params, array_values( $hooks ) );
		}

		$group_operator = 'IN';
		if ( empty( $group ) ) {
			$group          = $this->get_claim_filter( 'exclude-groups' );
			$group_operator = 'NOT IN';
		}

		if ( ! empty( $group ) ) {
			$group_ids = $this->get_group_ids( $group, false );

			// throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour.
			if ( empty( $group_ids ) ) {
				throw new InvalidArgumentException(
					sprintf(
						/* translators: %s: group name(s) */
						_n(
							'The group "%s" does not exist.',
							'The groups "%s" do not exist.',
							is_array( $group ) ? count( $group ) : 1,
							'action-scheduler'
						),
						$group
					)
				);
			}

			$id_list = implode( ',', array_map( 'intval', $group_ids ) );
			$where  .= " AND group_id {$group_operator} ( $id_list )";
		}

		/**
		 * Sets the order-by clause used in the action claim query.
		 *
		 * @since 3.4.0
		 * @since 3.8.3 Made $claim_id and $hooks available.
		 *
		 * @param string $order_by_sql
		 * @param string $claim_id Claim Id.
		 * @param array  $hooks Hooks to filter for.
		 */
		$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 );
		$params[] = $limit;

		$sql           = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
		$rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( false === $rows_affected ) {
			$error = empty( $wpdb->last_error )
				? _x( 'unknown', 'database error', 'action-scheduler' )
				: $wpdb->last_error;

			throw new \RuntimeException(
				sprintf(
					/* translators: %s database error. */
					__( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ),
					$error
				)
			);
		}

		return (int) $rows_affected;
	}

	/**
	 * Get the number of active claims.
	 *
	 * @return int
	 */
	public function get_claim_count() {
		global $wpdb;

		$sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)";
		$sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Return an action's claim ID, as stored in the claim_id column.
	 *
	 * @param string $action_id Action ID.
	 * @return mixed
	 */
	public function get_claim_id( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
		$sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Retrieve the action IDs of action in a claim.
	 *
	 * @param  int $claim_id Claim ID.
	 * @return int[]
	 */
	public function find_actions_by_claim_id( $claim_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$action_ids  = array();
		$before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
		$cut_off     = $before_date->format( 'Y-m-d H:i:s' );

		$sql = $wpdb->prepare(
			"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",
			$claim_id
		);

		// Verify that the scheduled date for each action is within the expected bounds (in some unusual
		// cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
		foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			if ( $claimed_action->scheduled_date_gmt <= $cut_off ) {
				$action_ids[] = absint( $claimed_action->action_id );
			}
		}

		return $action_ids;
	}

	/**
	 * Release actions from a claim and delete the claim.
	 *
	 * @param ActionScheduler_ActionClaim $claim Claim object.
	 * @throws \RuntimeException When unable to release actions from claim.
	 */
	public function release_claim( ActionScheduler_ActionClaim $claim ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		/**
		 * Deadlock warning: This function modifies actions to release them from claims that have been processed. Earlier, we used to it in a atomic query, i.e. we would update all actions belonging to a particular claim_id with claim_id = 0.
		 * While this was functionally correct, it would cause deadlock, since this update query will hold a lock on the claim_id_.. index on the action table.
		 * This allowed the possibility of a race condition, where the claimer query is also running at the same time, then the claimer query will also try to acquire a lock on the claim_id_.. index, and in this case if claim release query has already progressed to the point of acquiring the lock, but have not updated yet, it would cause a deadlock.
		 *
		 * We resolve this by getting all the actions_id that we want to release claim from in a separate query, and then releasing the claim on each of them. This way, our lock is acquired on the action_id index instead of the claim_id index. Note that the lock on claim_id will still be acquired, but it will only when we actually make the update, rather than when we select the actions.
		 */
		$action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", $claim->get_id() ) );

		$row_updates = 0;
		if ( count( $action_ids ) > 0 ) {
			$action_id_string = implode( ',', array_map( 'absint', $action_ids ) );
			$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
		}

		$wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) );

		if ( $row_updates < count( $action_ids ) ) {
			throw new RuntimeException(
				sprintf(
					// translators: %d is an id.
					__( 'Unable to release actions from claim id %d.', 'action-scheduler' ),
					$claim->get_id()
				)
			);
		}
	}

	/**
	 * Remove the claim from an action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return void
	 */
	public function unclaim_action( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$wpdb->update(
			$wpdb->actionscheduler_actions,
			array( 'claim_id' => 0 ),
			array( 'action_id' => $action_id ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Mark an action as failed.
	 *
	 * @param int $action_id Action ID.
	 * @throws \InvalidArgumentException Throw an exception if action was not updated.
	 */
	public function mark_failure( $action_id ) {
		/**
		 * Global.

		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$updated = $wpdb->update(
			$wpdb->actionscheduler_actions,
			array( 'status' => self::STATUS_FAILED ),
			array( 'action_id' => $action_id ),
			array( '%s' ),
			array( '%d' )
		);
		if ( empty( $updated ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
	}

	/**
	 * Add execution message to action log.
	 *
	 * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return void
	 */
	public function log_execution( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
		$sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$status_updated = $wpdb->query( $sql );

		if ( ! $status_updated ) {
			throw new Exception(
				sprintf(
					/* translators: 1: action ID. 2: status slug. */
					__( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
					$action_id,
					self::STATUS_RUNNING
				)
			);
		}
	}

	/**
	 * Mark an action as complete.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return void
	 * @throws \InvalidArgumentException Throw an exception if action was not updated.
	 */
	public function mark_complete( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$updated = $wpdb->update(
			$wpdb->actionscheduler_actions,
			array(
				'status'             => self::STATUS_COMPLETE,
				'last_attempt_gmt'   => current_time( 'mysql', true ),
				'last_attempt_local' => current_time( 'mysql' ),
			),
			array( 'action_id' => $action_id ),
			array( '%s' ),
			array( '%d' )
		);
		if ( empty( $updated ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}

		/**
		 * Fires after a scheduled action has been completed.
		 *
		 * @since 3.4.2
		 *
		 * @param int $action_id Action ID.
		 */
		do_action( 'action_scheduler_completed_action', $action_id );
	}

	/**
	 * Get an action's status.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return string
	 * @throws \InvalidArgumentException Throw an exception if not status was found for action_id.
	 * @throws \RuntimeException Throw an exception if action status could not be retrieved.
	 */
	public function get_status( $action_id ) {
		/**
		 * Global.
		 *
		 * @var \wpdb $wpdb
		 */
		global $wpdb;

		$sql    = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
		$sql    = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( is_null( $status ) ) {
			throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
		} elseif ( empty( $status ) ) {
			throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) );
		} else {
			return $status;
		}
	}
}
ActionScheduler_HybridStore.php000064400000030557151547307000012663 0ustar00<?php

use ActionScheduler_Store as Store;
use Action_Scheduler\Migration\Runner;
use Action_Scheduler\Migration\Config;
use Action_Scheduler\Migration\Controller;

/**
 * Class ActionScheduler_HybridStore
 *
 * A wrapper around multiple stores that fetches data from both.
 *
 * @since 3.0.0
 */
class ActionScheduler_HybridStore extends Store {
	const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation';

	/**
	 * Primary store instance.
	 *
	 * @var ActionScheduler_Store
	 */
	private $primary_store;

	/**
	 * Secondary store instance.
	 *
	 * @var ActionScheduler_Store
	 */
	private $secondary_store;

	/**
	 * Runner instance.
	 *
	 * @var Action_Scheduler\Migration\Runner
	 */
	private $migration_runner;

	/**
	 * The dividing line between IDs of actions created
	 * by the primary and secondary stores.
	 *
	 * @var int
	 *
	 * Methods that accept an action ID will compare the ID against
	 * this to determine which store will contain that ID. In almost
	 * all cases, the ID should come from the primary store, but if
	 * client code is bypassing the API functions and fetching IDs
	 * from elsewhere, then there is a chance that an unmigrated ID
	 * might be requested.
	 */
	private $demarkation_id = 0;

	/**
	 * ActionScheduler_HybridStore constructor.
	 *
	 * @param Config|null $config Migration config object.
	 */
	public function __construct( ?Config $config = null ) {
		$this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 );
		if ( empty( $config ) ) {
			$config = Controller::instance()->get_migration_config_object();
		}
		$this->primary_store    = $config->get_destination_store();
		$this->secondary_store  = $config->get_source_store();
		$this->migration_runner = new Runner( $config );
	}

	/**
	 * Initialize the table data store tables.
	 *
	 * @codeCoverageIgnore
	 */
	public function init() {
		add_action( 'action_scheduler/created_table', array( $this, 'set_autoincrement' ), 10, 2 );
		$this->primary_store->init();
		$this->secondary_store->init();
		remove_action( 'action_scheduler/created_table', array( $this, 'set_autoincrement' ), 10 );
	}

	/**
	 * When the actions table is created, set its autoincrement
	 * value to be one higher than the posts table to ensure that
	 * there are no ID collisions.
	 *
	 * @param string $table_name Table name.
	 * @param string $table_suffix Suffix of table name.
	 *
	 * @return void
	 * @codeCoverageIgnore
	 */
	public function set_autoincrement( $table_name, $table_suffix ) {
		if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) {
			if ( empty( $this->demarkation_id ) ) {
				$this->demarkation_id = $this->set_demarkation_id();
			}

			/**
			 * Global.
			 *
			 * @var \wpdb $wpdb
			 */
			global $wpdb;

			/**
			 * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with
			 * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE.
			 */
			$default_date = new DateTime( 'tomorrow' );
			$null_action  = new ActionScheduler_NullAction();
			$date_gmt     = $this->get_scheduled_date_string( $null_action, $default_date );
			$date_local   = $this->get_scheduled_date_string_local( $null_action, $default_date );

			$row_count = $wpdb->insert(
				$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
				array(
					'action_id'            => $this->demarkation_id,
					'hook'                 => '',
					'status'               => '',
					'scheduled_date_gmt'   => $date_gmt,
					'scheduled_date_local' => $date_local,
					'last_attempt_gmt'     => $date_gmt,
					'last_attempt_local'   => $date_local,
				)
			);
			if ( $row_count > 0 ) {
				$wpdb->delete(
					$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
					array( 'action_id' => $this->demarkation_id )
				);
			}
		}
	}

	/**
	 * Store the demarkation id in WP options.
	 *
	 * @param int $id The ID to set as the demarkation point between the two stores
	 *                Leave null to use the next ID from the WP posts table.
	 *
	 * @return int The new ID.
	 *
	 * @codeCoverageIgnore
	 */
	private function set_demarkation_id( $id = null ) {
		if ( empty( $id ) ) {
			/**
			 * Global.
			 *
			 * @var \wpdb $wpdb
			 */
			global $wpdb;

			$id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" );
			$id++;
		}
		update_option( self::DEMARKATION_OPTION, $id );

		return $id;
	}

	/**
	 * Find the first matching action from the secondary store.
	 * If it exists, migrate it to the primary store immediately.
	 * After it migrates, the secondary store will logically contain
	 * the next matching action, so return the result thence.
	 *
	 * @param string $hook Action's hook.
	 * @param array  $params Action's arguments.
	 *
	 * @return string
	 */
	public function find_action( $hook, $params = array() ) {
		$found_unmigrated_action = $this->secondary_store->find_action( $hook, $params );
		if ( ! empty( $found_unmigrated_action ) ) {
			$this->migrate( array( $found_unmigrated_action ) );
		}

		return $this->primary_store->find_action( $hook, $params );
	}

	/**
	 * Find actions matching the query in the secondary source first.
	 * If any are found, migrate them immediately. Then the secondary
	 * store will contain the canonical results.
	 *
	 * @param array  $query Query arguments.
	 * @param string $query_type Whether to select or count the results. Default, select.
	 *
	 * @return int[]
	 */
	public function query_actions( $query = array(), $query_type = 'select' ) {
		$found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' );
		if ( ! empty( $found_unmigrated_actions ) ) {
			$this->migrate( $found_unmigrated_actions );
		}

		return $this->primary_store->query_actions( $query, $query_type );
	}

	/**
	 * Get a count of all actions in the store, grouped by status
	 *
	 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
	 */
	public function action_counts() {
		$unmigrated_actions_count = $this->secondary_store->action_counts();
		$migrated_actions_count   = $this->primary_store->action_counts();
		$actions_count_by_status  = array();

		foreach ( $this->get_status_labels() as $status_key => $status_label ) {

			$count = 0;

			if ( isset( $unmigrated_actions_count[ $status_key ] ) ) {
				$count += $unmigrated_actions_count[ $status_key ];
			}

			if ( isset( $migrated_actions_count[ $status_key ] ) ) {
				$count += $migrated_actions_count[ $status_key ];
			}

			$actions_count_by_status[ $status_key ] = $count;
		}

		$actions_count_by_status = array_filter( $actions_count_by_status );

		return $actions_count_by_status;
	}

	/**
	 * If any actions would have been claimed by the secondary store,
	 * migrate them immediately, then ask the primary store for the
	 * canonical claim.
	 *
	 * @param int           $max_actions Maximum number of actions to claim.
	 * @param null|DateTime $before_date Latest timestamp of actions to claim.
	 * @param string[]      $hooks Hook of actions to claim.
	 * @param string        $group Group of actions to claim.
	 *
	 * @return ActionScheduler_ActionClaim
	 */
	public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
		$claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group );

		$claimed_actions = $claim->get_actions();
		if ( ! empty( $claimed_actions ) ) {
			$this->migrate( $claimed_actions );
		}

		$this->secondary_store->release_claim( $claim );

		return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
	}

	/**
	 * Migrate a list of actions to the table data store.
	 *
	 * @param array $action_ids List of action IDs.
	 */
	private function migrate( $action_ids ) {
		$this->migration_runner->migrate_actions( $action_ids );
	}

	/**
	 * Save an action to the primary store.
	 *
	 * @param ActionScheduler_Action $action Action object to be saved.
	 * @param DateTime|null          $date Optional. Schedule date. Default null.
	 *
	 * @return int The action ID
	 */
	public function save_action( ActionScheduler_Action $action, ?DateTime $date = null ) {
		return $this->primary_store->save_action( $action, $date );
	}

	/**
	 * Retrieve an existing action whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function fetch_action( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id, true );
		if ( $store ) {
			return $store->fetch_action( $action_id );
		} else {
			return new ActionScheduler_NullAction();
		}
	}

	/**
	 * Cancel an existing action whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function cancel_action( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			$store->cancel_action( $action_id );
		}
	}

	/**
	 * Delete an existing action whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function delete_action( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			$store->delete_action( $action_id );
		}
	}

	/**
	 * Get the schedule date an existing action whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function get_date( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			return $store->get_date( $action_id );
		} else {
			return null;
		}
	}

	/**
	 * Mark an existing action as failed whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function mark_failure( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			$store->mark_failure( $action_id );
		}
	}

	/**
	 * Log the execution of an existing action whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function log_execution( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			$store->log_execution( $action_id );
		}
	}

	/**
	 * Mark an existing action complete whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function mark_complete( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			$store->mark_complete( $action_id );
		}
	}

	/**
	 * Get an existing action status whether migrated or not.
	 *
	 * @param int $action_id Action ID.
	 */
	public function get_status( $action_id ) {
		$store = $this->get_store_from_action_id( $action_id );
		if ( $store ) {
			return $store->get_status( $action_id );
		}
		return null;
	}

	/**
	 * Return which store an action is stored in.
	 *
	 * @param int  $action_id ID of the action.
	 * @param bool $primary_first Optional flag indicating search the primary store first.
	 * @return ActionScheduler_Store
	 */
	protected function get_store_from_action_id( $action_id, $primary_first = false ) {
		if ( $primary_first ) {
			$stores = array(
				$this->primary_store,
				$this->secondary_store,
			);
		} elseif ( $action_id < $this->demarkation_id ) {
			$stores = array(
				$this->secondary_store,
				$this->primary_store,
			);
		} else {
			$stores = array(
				$this->primary_store,
			);
		}

		foreach ( $stores as $store ) {
			$action = $store->fetch_action( $action_id );
			if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) {
				return $store;
			}
		}
		return null;
	}

	/**
	 * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * All claim-related functions should operate solely
	 * on the primary store.
	 * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */

	/**
	 * Get the claim count from the table data store.
	 */
	public function get_claim_count() {
		return $this->primary_store->get_claim_count();
	}

	/**
	 * Retrieve the claim ID for an action from the table data store.
	 *
	 * @param int $action_id Action ID.
	 */
	public function get_claim_id( $action_id ) {
		return $this->primary_store->get_claim_id( $action_id );
	}

	/**
	 * Release a claim in the table data store.
	 *
	 * @param ActionScheduler_ActionClaim $claim Claim object.
	 */
	public function release_claim( ActionScheduler_ActionClaim $claim ) {
		$this->primary_store->release_claim( $claim );
	}

	/**
	 * Release claims on an action in the table data store.
	 *
	 * @param int $action_id Action ID.
	 */
	public function unclaim_action( $action_id ) {
		$this->primary_store->unclaim_action( $action_id );
	}

	/**
	 * Retrieve a list of action IDs by claim.
	 *
	 * @param int $claim_id Claim ID.
	 */
	public function find_actions_by_claim_id( $claim_id ) {
		return $this->primary_store->find_actions_by_claim_id( $claim_id );
	}
}
ActionScheduler_wpCommentLogger.php000064400000017033151547307000013530 0ustar00<?php

/**
 * Class ActionScheduler_wpCommentLogger
 */
class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
	const AGENT = 'ActionScheduler';
	const TYPE  = 'action_log';

	/**
	 * Create log entry.
	 *
	 * @param string        $action_id Action ID.
	 * @param string        $message   Action log's message.
	 * @param DateTime|null $date      Action log's timestamp.
	 *
	 * @return string The log entry ID
	 */
	public function log( $action_id, $message, ?DateTime $date = null ) {
		if ( empty( $date ) ) {
			$date = as_get_datetime_object();
		} else {
			$date = as_get_datetime_object( clone $date );
		}
		$comment_id = $this->create_wp_comment( $action_id, $message, $date );
		return $comment_id;
	}

	/**
	 * Create comment.
	 *
	 * @param int      $action_id Action ID.
	 * @param string   $message Action log's message.
	 * @param DateTime $date Action log entry's timestamp.
	 */
	protected function create_wp_comment( $action_id, $message, DateTime $date ) {

		$comment_date_gmt = $date->format( 'Y-m-d H:i:s' );
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
		$comment_data = array(
			'comment_post_ID'  => $action_id,
			'comment_date'     => $date->format( 'Y-m-d H:i:s' ),
			'comment_date_gmt' => $comment_date_gmt,
			'comment_author'   => self::AGENT,
			'comment_content'  => $message,
			'comment_agent'    => self::AGENT,
			'comment_type'     => self::TYPE,
		);

		return wp_insert_comment( $comment_data );
	}

	/**
	 * Get single log entry for action.
	 *
	 * @param string $entry_id Entry ID.
	 *
	 * @return ActionScheduler_LogEntry
	 */
	public function get_entry( $entry_id ) {
		$comment = $this->get_comment( $entry_id );

		if ( empty( $comment ) || self::TYPE !== $comment->comment_type ) {
			return new ActionScheduler_NullLogEntry();
		}

		$date = as_get_datetime_object( $comment->comment_date_gmt );
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
		return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date );
	}

	/**
	 * Get action's logs.
	 *
	 * @param string $action_id Action ID.
	 *
	 * @return ActionScheduler_LogEntry[]
	 */
	public function get_logs( $action_id ) {
		$status = 'all';
		$logs   = array();

		if ( get_post_status( $action_id ) === 'trash' ) {
			$status = 'post-trashed';
		}

		$comments = get_comments(
			array(
				'post_id' => $action_id,
				'orderby' => 'comment_date_gmt',
				'order'   => 'ASC',
				'type'    => self::TYPE,
				'status'  => $status,
			)
		);

		foreach ( $comments as $c ) {
			$entry = $this->get_entry( $c );

			if ( ! empty( $entry ) ) {
				$logs[] = $entry;
			}
		}

		return $logs;
	}

	/**
	 * Get comment.
	 *
	 * @param int $comment_id Comment ID.
	 */
	protected function get_comment( $comment_id ) {
		return get_comment( $comment_id );
	}

	/**
	 * Filter comment queries.
	 *
	 * @param WP_Comment_Query $query Comment query object.
	 */
	public function filter_comment_queries( $query ) {
		foreach ( array( 'ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID' ) as $key ) {
			if ( ! empty( $query->query_vars[ $key ] ) ) {
				return; // don't slow down queries that wouldn't include action_log comments anyway.
			}
		}
		$query->query_vars['action_log_filter'] = true;
		add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 );
	}

	/**
	 * Filter comment queries.
	 *
	 * @param array            $clauses Query's clauses.
	 * @param WP_Comment_Query $query Query object.
	 *
	 * @return array
	 */
	public function filter_comment_query_clauses( $clauses, $query ) {
		if ( ! empty( $query->query_vars['action_log_filter'] ) ) {
			$clauses['where'] .= $this->get_where_clause();
		}
		return $clauses;
	}

	/**
	 * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not
	 * the WP_Comment_Query class handled by @see self::filter_comment_queries().
	 *
	 * @param string   $where Query's `where` clause.
	 * @param WP_Query $query Query object.
	 *
	 * @return string
	 */
	public function filter_comment_feed( $where, $query ) {
		if ( is_comment_feed() ) {
			$where .= $this->get_where_clause();
		}
		return $where;
	}

	/**
	 * Return a SQL clause to exclude Action Scheduler comments.
	 *
	 * @return string
	 */
	protected function get_where_clause() {
		global $wpdb;
		return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE );
	}

	/**
	 * Remove action log entries from wp_count_comments()
	 *
	 * @param array $stats   Comment count.
	 * @param int   $post_id Post ID.
	 *
	 * @return object
	 */
	public function filter_comment_count( $stats, $post_id ) {
		global $wpdb;

		if ( 0 === $post_id ) {
			$stats = $this->get_comment_count();
		}

		return $stats;
	}

	/**
	 * Retrieve the comment counts from our cache, or the database if the cached version isn't set.
	 *
	 * @return object
	 */
	protected function get_comment_count() {
		global $wpdb;

		$stats = get_transient( 'as_comment_count' );

		if ( ! $stats ) {
			$stats    = array();
			$count    = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A );
			$total    = 0;
			$stats    = array();
			$approved = array(
				'0'            => 'moderated',
				'1'            => 'approved',
				'spam'         => 'spam',
				'trash'        => 'trash',
				'post-trashed' => 'post-trashed',
			);

			foreach ( (array) $count as $row ) {
				// Don't count post-trashed toward totals.
				if ( 'post-trashed' !== $row['comment_approved'] && 'trash' !== $row['comment_approved'] ) {
					$total += $row['num_comments'];
				}
				if ( isset( $approved[ $row['comment_approved'] ] ) ) {
					$stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments'];
				}
			}

			$stats['total_comments'] = $total;
			$stats['all']            = $total;

			foreach ( $approved as $key ) {
				if ( empty( $stats[ $key ] ) ) {
					$stats[ $key ] = 0;
				}
			}

			$stats = (object) $stats;
			set_transient( 'as_comment_count', $stats );
		}

		return $stats;
	}

	/**
	 * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache
	 * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called.
	 */
	public function delete_comment_count_cache() {
		delete_transient( 'as_comment_count' );
	}

	/**
	 * Initialize.
	 *
	 * @codeCoverageIgnore
	 */
	public function init() {
		add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 );
		add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 );

		parent::init();

		add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 );
		add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs.
		add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 );

		// Delete comments count cache whenever there is a new comment or a comment status changes.
		add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) );
		add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) );
	}

	/**
	 * Defer comment counting.
	 */
	public function disable_comment_counting() {
		wp_defer_comment_counting( true );
	}

	/**
	 * Enable comment counting.
	 */
	public function enable_comment_counting() {
		wp_defer_comment_counting( false );
	}

}
ActionScheduler_wpPostStore.php000064400000106256151547307000012736 0ustar00<?php

/**
 * Class ActionScheduler_wpPostStore
 */
class ActionScheduler_wpPostStore extends ActionScheduler_Store {
	const POST_TYPE         = 'scheduled-action';
	const GROUP_TAXONOMY    = 'action-group';
	const SCHEDULE_META_KEY = '_action_manager_schedule';
	const DEPENDENCIES_MET  = 'as-post-store-dependencies-met';

	/**
	 * Used to share information about the before_date property of claims internally.
	 *
	 * This is used in preference to passing the same information as a method param
	 * for backwards-compatibility reasons.
	 *
	 * @var DateTime|null
	 */
	private $claim_before_date = null;

	/**
	 * Local Timezone.
	 *
	 * @var DateTimeZone
	 */
	protected $local_timezone = null;

	/**
	 * Save action.
	 *
	 * @param ActionScheduler_Action $action Scheduled Action.
	 * @param DateTime|null          $scheduled_date Scheduled Date.
	 *
	 * @throws RuntimeException Throws an exception if the action could not be saved.
	 * @return int
	 */
	public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		try {
			$this->validate_action( $action );
			$post_array = $this->create_post_array( $action, $scheduled_date );
			$post_id    = $this->save_post_array( $post_array );
			$this->save_post_schedule( $post_id, $action->get_schedule() );
			$this->save_action_group( $post_id, $action->get_group() );
			do_action( 'action_scheduler_stored_action', $post_id );
			return $post_id;
		} catch ( Exception $e ) {
			/* translators: %s: action error message */
			throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
		}
	}

	/**
	 * Create post array.
	 *
	 * @param ActionScheduler_Action $action Scheduled Action.
	 * @param DateTime|null          $scheduled_date Scheduled Date.
	 *
	 * @return array Returns an array of post data.
	 */
	protected function create_post_array( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
		$post = array(
			'post_type'     => self::POST_TYPE,
			'post_title'    => $action->get_hook(),
			'post_content'  => wp_json_encode( $action->get_args() ),
			'post_status'   => ( $action->is_finished() ? 'publish' : 'pending' ),
			'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ),
			'post_date'     => $this->get_scheduled_date_string_local( $action, $scheduled_date ),
		);
		return $post;
	}

	/**
	 * Save post array.
	 *
	 * @param array $post_array Post array.
	 * @return int Returns the post ID.
	 * @throws RuntimeException Throws an exception if the action could not be saved.
	 */
	protected function save_post_array( $post_array ) {
		add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
		add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );

		$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );

		if ( $has_kses ) {
			// Prevent KSES from corrupting JSON in post_content.
			kses_remove_filters();
		}

		$post_id = wp_insert_post( $post_array );

		if ( $has_kses ) {
			kses_init_filters();
		}

		remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
		remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );

		if ( is_wp_error( $post_id ) || empty( $post_id ) ) {
			throw new RuntimeException( __( 'Unable to save action.', 'action-scheduler' ) );
		}
		return $post_id;
	}

	/**
	 * Filter insert post data.
	 *
	 * @param array $postdata Post data to filter.
	 *
	 * @return array
	 */
	public function filter_insert_post_data( $postdata ) {
		if ( self::POST_TYPE === $postdata['post_type'] ) {
			$postdata['post_author'] = 0;
			if ( 'future' === $postdata['post_status'] ) {
				$postdata['post_status'] = 'publish';
			}
		}
		return $postdata;
	}

	/**
	 * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug().
	 *
	 * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish'
	 * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug()
	 * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing
	 * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a
	 * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a
	 * database containing thousands of related post_name values.
	 *
	 * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue.
	 *
	 * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This
	 * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a
	 * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an
	 * action's slug, being probably unique is good enough.
	 *
	 * For more backstory on this issue, see:
	 * - https://github.com/woocommerce/action-scheduler/issues/44 and
	 * - https://core.trac.wordpress.org/ticket/21112
	 *
	 * @param string $override_slug Short-circuit return value.
	 * @param string $slug          The desired slug (post_name).
	 * @param int    $post_ID       Post ID.
	 * @param string $post_status   The post status.
	 * @param string $post_type     Post type.
	 * @return string
	 */
	public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
		if ( self::POST_TYPE === $post_type ) {
			$override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false );
		}
		return $override_slug;
	}

	/**
	 * Save post schedule.
	 *
	 * @param int    $post_id  Post ID of the scheduled action.
	 * @param string $schedule Schedule to save.
	 *
	 * @return void
	 */
	protected function save_post_schedule( $post_id, $schedule ) {
		update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule );
	}

	/**
	 * Save action group.
	 *
	 * @param int    $post_id Post ID.
	 * @param string $group   Group to save.
	 * @return void
	 */
	protected function save_action_group( $post_id, $group ) {
		if ( empty( $group ) ) {
			wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, false );
		} else {
			wp_set_object_terms( $post_id, array( $group ), self::GROUP_TAXONOMY, false );
		}
	}

	/**
	 * Fetch actions.
	 *
	 * @param int $action_id Action ID.
	 * @return object
	 */
	public function fetch_action( $action_id ) {
		$post = $this->get_post( $action_id );
		if ( empty( $post ) || self::POST_TYPE !== $post->post_type ) {
			return $this->get_null_action();
		}

		try {
			$action = $this->make_action_from_post( $post );
		} catch ( ActionScheduler_InvalidActionException $exception ) {
			do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception );
			return $this->get_null_action();
		}

		return $action;
	}

	/**
	 * Get post.
	 *
	 * @param string $action_id - Action ID.
	 * @return WP_Post|null
	 */
	protected function get_post( $action_id ) {
		if ( empty( $action_id ) ) {
			return null;
		}
		return get_post( $action_id );
	}

	/**
	 * Get NULL action.
	 *
	 * @return ActionScheduler_NullAction
	 */
	protected function get_null_action() {
		return new ActionScheduler_NullAction();
	}

	/**
	 * Make action from post.
	 *
	 * @param WP_Post $post Post object.
	 * @return WP_Post
	 */
	protected function make_action_from_post( $post ) {
		$hook = $post->post_title;

		$args = json_decode( $post->post_content, true );
		$this->validate_args( $args, $post->ID );

		$schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true );
		$this->validate_schedule( $schedule, $post->ID );

		$group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array( 'fields' => 'names' ) );
		$group = empty( $group ) ? '' : reset( $group );

		return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group );
	}

	/**
	 * Get action status by post status.
	 *
	 * @param string $post_status Post status.
	 *
	 * @throws InvalidArgumentException Throw InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels().
	 * @return string
	 */
	protected function get_action_status_by_post_status( $post_status ) {

		switch ( $post_status ) {
			case 'publish':
				$action_status = self::STATUS_COMPLETE;
				break;
			case 'trash':
				$action_status = self::STATUS_CANCELED;
				break;
			default:
				if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) {
					throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) );
				}
				$action_status = $post_status;
				break;
		}

		return $action_status;
	}

	/**
	 * Get post status by action status.
	 *
	 * @param string $action_status Action status.
	 *
	 * @throws InvalidArgumentException Throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels().
	 * @return string
	 */
	protected function get_post_status_by_action_status( $action_status ) {

		switch ( $action_status ) {
			case self::STATUS_COMPLETE:
				$post_status = 'publish';
				break;
			case self::STATUS_CANCELED:
				$post_status = 'trash';
				break;
			default:
				if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) {
					throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) );
				}
				$post_status = $action_status;
				break;
		}

		return $post_status;
	}

	/**
	 * Returns the SQL statement to query (or count) actions.
	 *
	 * @param array  $query            - Filtering options.
	 * @param string $select_or_count  - Whether the SQL should select and return the IDs or just the row count.
	 *
	 * @throws InvalidArgumentException - Throw InvalidArgumentException if $select_or_count not count or select.
	 * @return string SQL statement. The returned SQL is already properly escaped.
	 */
	protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {

		if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
			throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) );
		}

		$query = wp_parse_args(
			$query,
			array(
				'hook'             => '',
				'args'             => null,
				'date'             => null,
				'date_compare'     => '<=',
				'modified'         => null,
				'modified_compare' => '<=',
				'group'            => '',
				'status'           => '',
				'claimed'          => null,
				'per_page'         => 5,
				'offset'           => 0,
				'orderby'          => 'date',
				'order'            => 'ASC',
				'search'           => '',
			)
		);

		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;
		$sql        = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID ';
		$sql       .= "FROM {$wpdb->posts} p";
		$sql_params = array();
		if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) {
			$sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
			$sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
			$sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
		} elseif ( ! empty( $query['group'] ) ) {
			$sql         .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
			$sql         .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
			$sql         .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
			$sql         .= ' AND t.slug=%s';
			$sql_params[] = $query['group'];
		}
		$sql         .= ' WHERE post_type=%s';
		$sql_params[] = self::POST_TYPE;
		if ( $query['hook'] ) {
			$sql         .= ' AND p.post_title=%s';
			$sql_params[] = $query['hook'];
		}
		if ( ! is_null( $query['args'] ) ) {
			$sql         .= ' AND p.post_content=%s';
			$sql_params[] = wp_json_encode( $query['args'] );
		}

		if ( $query['status'] ) {
			$post_statuses = array_map( array( $this, 'get_post_status_by_action_status' ), (array) $query['status'] );
			$placeholders  = array_fill( 0, count( $post_statuses ), '%s' );
			$sql          .= ' AND p.post_status IN (' . join( ', ', $placeholders ) . ')';
			$sql_params    = array_merge( $sql_params, array_values( $post_statuses ) );
		}

		if ( $query['date'] instanceof DateTime ) {
			$date = clone $query['date'];
			$date->setTimezone( new DateTimeZone( 'UTC' ) );
			$date_string  = $date->format( 'Y-m-d H:i:s' );
			$comparator   = $this->validate_sql_comparator( $query['date_compare'] );
			$sql         .= " AND p.post_date_gmt $comparator %s";
			$sql_params[] = $date_string;
		}

		if ( $query['modified'] instanceof DateTime ) {
			$modified = clone $query['modified'];
			$modified->setTimezone( new DateTimeZone( 'UTC' ) );
			$date_string  = $modified->format( 'Y-m-d H:i:s' );
			$comparator   = $this->validate_sql_comparator( $query['modified_compare'] );
			$sql         .= " AND p.post_modified_gmt $comparator %s";
			$sql_params[] = $date_string;
		}

		if ( true === $query['claimed'] ) {
			$sql .= " AND p.post_password != ''";
		} elseif ( false === $query['claimed'] ) {
			$sql .= " AND p.post_password = ''";
		} elseif ( ! is_null( $query['claimed'] ) ) {
			$sql         .= ' AND p.post_password = %s';
			$sql_params[] = $query['claimed'];
		}

		if ( ! empty( $query['search'] ) ) {
			$sql .= ' AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)';
			for ( $i = 0; $i < 3; $i++ ) {
				$sql_params[] = sprintf( '%%%s%%', $query['search'] );
			}
		}

		if ( 'select' === $select_or_count ) {
			switch ( $query['orderby'] ) {
				case 'hook':
					$orderby = 'p.post_title';
					break;
				case 'group':
					$orderby = 't.name';
					break;
				case 'status':
					$orderby = 'p.post_status';
					break;
				case 'modified':
					$orderby = 'p.post_modified';
					break;
				case 'claim_id':
					$orderby = 'p.post_password';
					break;
				case 'schedule':
				case 'date':
				default:
					$orderby = 'p.post_date_gmt';
					break;
			}
			if ( 'ASC' === strtoupper( $query['order'] ) ) {
				$order = 'ASC';
			} else {
				$order = 'DESC';
			}
			$sql .= " ORDER BY $orderby $order";
			if ( $query['per_page'] > 0 ) {
				$sql         .= ' LIMIT %d, %d';
				$sql_params[] = $query['offset'];
				$sql_params[] = $query['per_page'];
			}
		}

		return $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Query for action count or list of action IDs.
	 *
	 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
	 *
	 * @see ActionScheduler_Store::query_actions for $query arg usage.
	 *
	 * @param array  $query      Query filtering options.
	 * @param string $query_type Whether to select or count the results. Defaults to select.
	 *
	 * @return string|array|null The IDs of actions matching the query. Null on failure.
	 */
	public function query_actions( $query = array(), $query_type = 'select' ) {
		/**
		 * Global $wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		$sql = $this->get_query_actions_sql( $query, $query_type );

		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
	}

	/**
	 * Get a count of all actions in the store, grouped by status
	 *
	 * @return array
	 */
	public function action_counts() {

		$action_counts_by_status = array();
		$action_stati_and_labels = $this->get_status_labels();
		$posts_count_by_status   = (array) wp_count_posts( self::POST_TYPE, 'readable' );

		foreach ( $posts_count_by_status as $post_status_name => $count ) {

			try {
				$action_status_name = $this->get_action_status_by_post_status( $post_status_name );
			} catch ( Exception $e ) {
				// Ignore any post statuses that aren't for actions.
				continue;
			}
			if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) {
				$action_counts_by_status[ $action_status_name ] = $count;
			}
		}

		return $action_counts_by_status;
	}

	/**
	 * Cancel action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @throws InvalidArgumentException If $action_id is not identified.
	 */
	public function cancel_action( $action_id ) {
		$post = get_post( $action_id );
		if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		do_action( 'action_scheduler_canceled_action', $action_id );
		add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
		wp_trash_post( $action_id );
		remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
	}

	/**
	 * Delete action.
	 *
	 * @param int $action_id Action ID.
	 * @return void
	 * @throws InvalidArgumentException If action is not identified.
	 */
	public function delete_action( $action_id ) {
		$post = get_post( $action_id );
		if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		do_action( 'action_scheduler_deleted_action', $action_id );

		wp_delete_post( $action_id, true );
	}

	/**
	 * Get date for claim id.
	 *
	 * @param int $action_id Action ID.
	 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
	 */
	public function get_date( $action_id ) {
		$next = $this->get_date_gmt( $action_id );
		return ActionScheduler_TimezoneHelper::set_local_timezone( $next );
	}

	/**
	 * Get Date GMT.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @throws InvalidArgumentException If $action_id is not identified.
	 * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran.
	 */
	public function get_date_gmt( $action_id ) {
		$post = get_post( $action_id );
		if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		if ( 'publish' === $post->post_status ) {
			return as_get_datetime_object( $post->post_modified_gmt );
		} else {
			return as_get_datetime_object( $post->post_date_gmt );
		}
	}

	/**
	 * Stake claim.
	 *
	 * @param int           $max_actions Maximum number of actions.
	 * @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now.
	 * @param array         $hooks       Claim only actions with a hook or hooks.
	 * @param string        $group       Claim only actions in the given group.
	 *
	 * @return ActionScheduler_ActionClaim
	 * @throws RuntimeException When there is an error staking a claim.
	 * @throws InvalidArgumentException When the given group is not valid.
	 */
	public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
		$this->claim_before_date = $before_date;
		$claim_id                = $this->generate_claim_id();
		$this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
		$action_ids              = $this->find_actions_by_claim_id( $claim_id );
		$this->claim_before_date = null;

		return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
	}

	/**
	 * Get claim count.
	 *
	 * @return int
	 */
	public function get_claim_count() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')",
				array( self::POST_TYPE )
			)
		);
	}

	/**
	 * Generate claim id.
	 *
	 * @return string
	 */
	protected function generate_claim_id() {
		$claim_id = md5( microtime( true ) . wp_rand( 0, 1000 ) );
		return substr( $claim_id, 0, 20 ); // to fit in db field with 20 char limit.
	}

	/**
	 * Claim actions.
	 *
	 * @param string        $claim_id    Claim ID.
	 * @param int           $limit       Limit.
	 * @param DateTime|null $before_date Should use UTC timezone.
	 * @param array         $hooks       Claim only actions with a hook or hooks.
	 * @param string        $group       Claim only actions in the given group.
	 *
	 * @return int The number of actions that were claimed.
	 * @throws RuntimeException  When there is a database error.
	 */
	protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
		// Set up initial variables.
		$date      = null === $before_date ? as_get_datetime_object() : clone $before_date;
		$limit_ids = ! empty( $group );
		$ids       = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array();

		// If limiting by IDs and no posts found, then return early since we have nothing to update.
		if ( $limit_ids && 0 === count( $ids ) ) {
			return 0;
		}

		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		/*
		 * Build up custom query to update the affected posts. Parameters are built as a separate array
		 * to make it easier to identify where they are in the query.
		 *
		 * We can't use $wpdb->update() here because of the "ID IN ..." clause.
		 */
		$update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s";
		$params = array(
			$claim_id,
			current_time( 'mysql', true ),
			current_time( 'mysql' ),
		);

		// Build initial WHERE clause.
		$where    = "WHERE post_type = %s AND post_status = %s AND post_password = ''";
		$params[] = self::POST_TYPE;
		$params[] = ActionScheduler_Store::STATUS_PENDING;

		if ( ! empty( $hooks ) ) {
			$placeholders = array_fill( 0, count( $hooks ), '%s' );
			$where       .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')';
			$params       = array_merge( $params, array_values( $hooks ) );
		}

		/*
		 * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query.
		 *
		 * If we're not limiting by IDs, then include the post_date_gmt clause.
		 */
		if ( $limit_ids ) {
			$where .= ' AND ID IN (' . join( ',', $ids ) . ')';
		} else {
			$where   .= ' AND post_date_gmt <= %s';
			$params[] = $date->format( 'Y-m-d H:i:s' );
		}

		// Add the ORDER BY clause and,ms limit.
		$order    = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d';
		$params[] = $limit;

		// Run the query and gather results.
		$rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

		if ( false === $rows_affected ) {
			throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
		}

		return (int) $rows_affected;
	}

	/**
	 * Get IDs of actions within a certain group and up to a certain date/time.
	 *
	 * @param string   $group The group to use in finding actions.
	 * @param int      $limit The number of actions to retrieve.
	 * @param DateTime $date  DateTime object representing cutoff time for actions. Actions retrieved will be
	 *                        up to and including this DateTime.
	 *
	 * @return array IDs of actions in the appropriate group and before the appropriate time.
	 * @throws InvalidArgumentException When the group does not exist.
	 */
	protected function get_actions_by_group( $group, $limit, DateTime $date ) {
		// Ensure the group exists before continuing.
		if ( ! term_exists( $group, self::GROUP_TAXONOMY ) ) {
			/* translators: %s is the group name */
			throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
		}

		// Set up a query for post IDs to use later.
		$query      = new WP_Query();
		$query_args = array(
			'fields'           => 'ids',
			'post_type'        => self::POST_TYPE,
			'post_status'      => ActionScheduler_Store::STATUS_PENDING,
			'has_password'     => false,
			'posts_per_page'   => $limit * 3,
			'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
			'no_found_rows'    => true,
			'orderby'          => array(
				'menu_order' => 'ASC',
				'date'       => 'ASC',
				'ID'         => 'ASC',
			),
			'date_query'       => array(
				'column'    => 'post_date_gmt',
				'before'    => $date->format( 'Y-m-d H:i' ),
				'inclusive' => true,
			),
			'tax_query'        => array( // phpcs:ignore WordPress.DB.SlowDBQuery
				array(
					'taxonomy'         => self::GROUP_TAXONOMY,
					'field'            => 'slug',
					'terms'            => $group,
					'include_children' => false,
				),
			),
		);

		return $query->query( $query_args );
	}

	/**
	 * Find actions by claim ID.
	 *
	 * @param string $claim_id Claim ID.
	 * @return array
	 */
	public function find_actions_by_claim_id( $claim_id ) {
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		$action_ids  = array();
		$before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
		$cut_off     = $before_date->format( 'Y-m-d H:i:s' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s",
				array(
					self::POST_TYPE,
					$claim_id,
				)
			)
		);

		// Verify that the scheduled date for each action is within the expected bounds (in some unusual
		// cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
		foreach ( $results as $claimed_action ) {
			if ( $claimed_action->post_date_gmt <= $cut_off ) {
				$action_ids[] = absint( $claimed_action->ID );
			}
		}

		return $action_ids;
	}

	/**
	 * Release claim.
	 *
	 * @param ActionScheduler_ActionClaim $claim Claim object to release.
	 * @return void
	 * @throws RuntimeException When the claim is not unlocked.
	 */
	public function release_claim( ActionScheduler_ActionClaim $claim ) {
		$action_ids = $this->find_actions_by_claim_id( $claim->get_id() );
		if ( empty( $action_ids ) ) {
			return; // nothing to do.
		}
		$action_id_string = implode( ',', array_map( 'intval', $action_ids ) );
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s", //phpcs:ignore
				array(
					$claim->get_id(),
				)
			)
		);
		if ( false === $result ) {
			/* translators: %s: claim ID */
			throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'action-scheduler' ), $claim->get_id() ) );
		}
	}

	/**
	 * Unclaim action.
	 *
	 * @param string $action_id Action ID.
	 * @throws RuntimeException When unable to unlock claim on action ID.
	 */
	public function unclaim_action( $action_id ) {
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		//phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s",
				$action_id,
				self::POST_TYPE
			)
		);
		if ( false === $result ) {
			/* translators: %s: action ID */
			throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'action-scheduler' ), $action_id ) );
		}
	}

	/**
	 * Mark failure on action.
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return void
	 * @throws RuntimeException When unable to mark failure on action ID.
	 */
	public function mark_failure( $action_id ) {
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->query(
			$wpdb->prepare( "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s", self::STATUS_FAILED, $action_id, self::POST_TYPE )
		);
		if ( false === $result ) {
			/* translators: %s: action ID */
			throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'action-scheduler' ), $action_id ) );
		}
	}

	/**
	 * Return an action's claim ID, as stored in the post password column
	 *
	 * @param int $action_id Action ID.
	 * @return mixed
	 */
	public function get_claim_id( $action_id ) {
		return $this->get_post_column( $action_id, 'post_password' );
	}

	/**
	 * Return an action's status, as stored in the post status column
	 *
	 * @param int $action_id Action ID.
	 *
	 * @return mixed
	 * @throws InvalidArgumentException When the action ID is invalid.
	 */
	public function get_status( $action_id ) {
		$status = $this->get_post_column( $action_id, 'post_status' );

		if ( null === $status ) {
			throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
		}

		return $this->get_action_status_by_post_status( $status );
	}

	/**
	 * Get post column
	 *
	 * @param string $action_id Action ID.
	 * @param string $column_name Column Name.
	 *
	 * @return string|null
	 */
	private function get_post_column( $action_id, $column_name ) {
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", // phpcs:ignore
				$action_id,
				self::POST_TYPE
			)
		);
	}

	/**
	 * Log Execution.
	 *
	 * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
	 *
	 * @param string $action_id Action ID.
	 */
	public function log_execution( $action_id ) {
		/**
		 * Global wpdb object.
		 *
		 * @var wpdb $wpdb
		 */
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$status_updated = $wpdb->query(
			$wpdb->prepare(
				"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",
				self::STATUS_RUNNING,
				current_time( 'mysql', true ),
				current_time( 'mysql' ),
				$action_id,
				self::POST_TYPE
			)
		);

		if ( ! $status_updated ) {
			throw new Exception(
				sprintf(
					/* translators: 1: action ID. 2: status slug. */
					__( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
					$action_id,
					self::STATUS_RUNNING
				)
			);
		}
	}

	/**
	 * Record that an action was completed.
	 *
	 * @param string $action_id ID of the completed action.
	 *
	 * @throws InvalidArgumentException When the action ID is invalid.
	 * @throws RuntimeException         When there was an error executing the action.
	 */
	public function mark_complete( $action_id ) {
		$post = get_post( $action_id );
		if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) {
			/* translators: %s is the action ID */
			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 ) );
		}
		add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
		add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
		$result = wp_update_post(
			array(
				'ID'          => $action_id,
				'post_status' => 'publish',
			),
			true
		);
		remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
		remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
		if ( is_wp_error( $result ) ) {
			throw new RuntimeException( $result->get_error_message() );
		}

		/**
		 * Fires after a scheduled action has been completed.
		 *
		 * @since 3.4.2
		 *
		 * @param int $action_id Action ID.
		 */
		do_action( 'action_scheduler_completed_action', $action_id );
	}

	/**
	 * Mark action as migrated when there is an error deleting the action.
	 *
	 * @param int $action_id Action ID.
	 */
	public function mark_migrated( $action_id ) {
		wp_update_post(
			array(
				'ID'          => $action_id,
				'post_status' => 'migrated',
			)
		);
	}

	/**
	 * Determine whether the post store can be migrated.
	 *
	 * @param [type] $setting - Setting value.
	 * @return bool
	 */
	public function migration_dependencies_met( $setting ) {
		global $wpdb;

		$dependencies_met = get_transient( self::DEPENDENCIES_MET );
		if ( empty( $dependencies_met ) ) {
			$maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 );
			$found_action        = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->prepare(
					"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1",
					$maximum_args_length,
					self::POST_TYPE
				)
			);
			$dependencies_met    = $found_action ? 'no' : 'yes';
			set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS );
		}

		return 'yes' === $dependencies_met ? $setting : false;
	}

	/**
	 * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4.
	 *
	 * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However,
	 * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn
	 * developers of this impending requirement.
	 *
	 * @param ActionScheduler_Action $action Action object.
	 */
	protected function validate_action( ActionScheduler_Action $action ) {
		try {
			parent::validate_action( $action );
		} catch ( Exception $e ) {
			/* translators: %s is the error message */
			$message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'action-scheduler' ), $e->getMessage() );
			_doing_it_wrong( 'ActionScheduler_Action::$args', esc_html( $message ), '2.1.0' );
		}
	}

	/**
	 * (@codeCoverageIgnore)
	 */
	public function init() {
		add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) );

		$post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar();
		$post_type_registrar->register();

		$post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar();
		$post_status_registrar->register();

		$taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar();
		$taxonomy_registrar->register();
	}
}
ActionScheduler_wpPostStore_PostStatusRegistrar.php000064400000003502151547307000017020 0ustar00<?php

/**
 * Class ActionScheduler_wpPostStore_PostStatusRegistrar
 *
 * @codeCoverageIgnore
 */
class ActionScheduler_wpPostStore_PostStatusRegistrar {

	/**
	 * Registrar.
	 */
	public function register() {
		register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) );
		register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) );
	}

	/**
	 * Build the args array for the post type definition
	 *
	 * @return array
	 */
	protected function post_status_args() {
		$args = array(
			'public'                    => false,
			'exclude_from_search'       => false,
			'show_in_admin_all_list'    => true,
			'show_in_admin_status_list' => true,
		);

		return apply_filters( 'action_scheduler_post_status_args', $args );
	}

	/**
	 * Build the args array for the post type definition
	 *
	 * @return array
	 */
	protected function post_status_failed_labels() {
		$labels = array(
			'label'       => _x( 'Failed', 'post', 'action-scheduler' ),
			/* translators: %s: count */
			'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'action-scheduler' ),
		);

		return apply_filters( 'action_scheduler_post_status_failed_labels', $labels );
	}

	/**
	 * Build the args array for the post type definition
	 *
	 * @return array
	 */
	protected function post_status_running_labels() {
		$labels = array(
			'label'       => _x( 'In-Progress', 'post', 'action-scheduler' ),
			/* translators: %s: count */
			'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'action-scheduler' ),
		);

		return apply_filters( 'action_scheduler_post_status_running_labels', $labels );
	}
}
ActionScheduler_wpPostStore_PostTypeRegistrar.php000064400000003711151547307000016460 0ustar00<?php

/**
 * Class ActionScheduler_wpPostStore_PostTypeRegistrar
 *
 * @codeCoverageIgnore
 */
class ActionScheduler_wpPostStore_PostTypeRegistrar {
	/**
	 * Registrar.
	 */
	public function register() {
		register_post_type( ActionScheduler_wpPostStore::POST_TYPE, $this->post_type_args() );
	}

	/**
	 * Build the args array for the post type definition
	 *
	 * @return array
	 */
	protected function post_type_args() {
		$args = array(
			'label'        => __( 'Scheduled Actions', 'action-scheduler' ),
			'description'  => __( 'Scheduled actions are hooks triggered on a certain date and time.', 'action-scheduler' ),
			'public'       => false,
			'map_meta_cap' => true,
			'hierarchical' => false,
			'supports'     => array( 'title', 'editor', 'comments' ),
			'rewrite'      => false,
			'query_var'    => false,
			'can_export'   => true,
			'ep_mask'      => EP_NONE,
			'labels'       => array(
				'name'               => __( 'Scheduled Actions', 'action-scheduler' ),
				'singular_name'      => __( 'Scheduled Action', 'action-scheduler' ),
				'menu_name'          => _x( 'Scheduled Actions', 'Admin menu name', 'action-scheduler' ),
				'add_new'            => __( 'Add', 'action-scheduler' ),
				'add_new_item'       => __( 'Add New Scheduled Action', 'action-scheduler' ),
				'edit'               => __( 'Edit', 'action-scheduler' ),
				'edit_item'          => __( 'Edit Scheduled Action', 'action-scheduler' ),
				'new_item'           => __( 'New Scheduled Action', 'action-scheduler' ),
				'view'               => __( 'View Action', 'action-scheduler' ),
				'view_item'          => __( 'View Action', 'action-scheduler' ),
				'search_items'       => __( 'Search Scheduled Actions', 'action-scheduler' ),
				'not_found'          => __( 'No actions found', 'action-scheduler' ),
				'not_found_in_trash' => __( 'No actions found in trash', 'action-scheduler' ),
			),
		);

		$args = apply_filters( 'action_scheduler_post_type_args', $args );
		return $args;
	}
}
ActionScheduler_wpPostStore_TaxonomyRegistrar.php000064400000001372151547307000016510 0ustar00<?php

/**
 * Class ActionScheduler_wpPostStore_TaxonomyRegistrar
 *
 * @codeCoverageIgnore
 */
class ActionScheduler_wpPostStore_TaxonomyRegistrar {

	/**
	 * Registrar.
	 */
	public function register() {
		register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() );
	}

	/**
	 * Get taxonomy arguments.
	 */
	protected function taxonomy_args() {
		$args = array(
			'label'             => __( 'Action Group', 'action-scheduler' ),
			'public'            => false,
			'hierarchical'      => false,
			'show_admin_column' => true,
			'query_var'         => false,
			'rewrite'           => false,
		);

		$args = apply_filters( 'action_scheduler_taxonomy_args', $args );
		return $args;
	}
}