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/DB.tar
Installer.php000064400000003246151542112540007217 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration\Migrator;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\FirstInstallInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\InstallableInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Class Installer
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
class Installer implements Service, FirstInstallInterface, InstallableInterface {

	/**
	 * @var TableManager
	 */
	protected $table_manager;

	/**
	 * @var Migrator
	 */
	protected $migrator;

	/**
	 * Installer constructor.
	 *
	 * @param TableManager $table_manager
	 * @param Migrator     $migrator
	 */
	public function __construct( TableManager $table_manager, Migrator $migrator ) {
		$this->table_manager = $table_manager;
		$this->migrator      = $migrator;
	}

	/**
	 * Run installation logic for this class.
	 *
	 * @param string $old_version Previous version before updating.
	 * @param string $new_version Current version after updating.
	 */
	public function install( string $old_version, string $new_version ): void {
		foreach ( $this->table_manager->get_tables() as $table ) {
			$table->install();
		}

		// Run migrations.
		$this->migrator->migrate( $old_version, $new_version );
	}

	/**
	 * Logic to run when the plugin is first installed.
	 */
	public function first_install(): void {
		foreach ( $this->table_manager->get_tables() as $table ) {
			if ( $table instanceof FirstInstallInterface ) {
				$table->first_install();
			}
		}
	}
}
Migration/AbstractMigration.php000064400000001034151542112540012621 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractMigration
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.4.1
 */
abstract class AbstractMigration implements MigrationInterface {
	/**
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * AbstractMigration constructor.
	 *
	 * @param wpdb $wpdb The wpdb object.
	 */
	public function __construct( wpdb $wpdb ) {
		$this->wpdb = $wpdb;
	}
}
Migration/Migration20211228T1640692399.php000064400000005016151542112540013130 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingRateTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class Migration20211228T1640692399
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.12.2
 */
class Migration20211228T1640692399 extends AbstractMigration {

	/**
	 * @var ShippingRateTable
	 */
	protected $shipping_rate_table;

	/**
	 * @var OptionsInterface
	 */
	protected $options;

	/**
	 * Migration constructor.
	 *
	 * @param wpdb              $wpdb The wpdb object.
	 * @param ShippingRateTable $shipping_rate_table
	 * @param OptionsInterface  $options
	 */
	public function __construct( wpdb $wpdb, ShippingRateTable $shipping_rate_table, OptionsInterface $options ) {
		parent::__construct( $wpdb );
		$this->shipping_rate_table = $shipping_rate_table;
		$this->options             = $options;
	}


	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string {
		return '1.12.2';
	}

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void {
		if ( $this->shipping_rate_table->exists() ) {
			$mc_settings = $this->options->get( OptionsInterface::MERCHANT_CENTER );
			if ( ! is_array( $mc_settings ) ) {
				return;
			}

			if ( isset( $mc_settings['offers_free_shipping'] ) && false !== boolval( $mc_settings['offers_free_shipping'] ) && isset( $mc_settings['free_shipping_threshold'] ) ) {
				// Move the free shipping threshold from the options to the shipping rate table.
				$options_json = wp_json_encode( [ 'free_shipping_threshold' => (float) $mc_settings['free_shipping_threshold'] ] );

				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$this->wpdb->_escape( $this->shipping_rate_table->get_name() )}` SET `options`=%s WHERE 1=1", $options_json ) );
				// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
				// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}

			// Remove the free shipping threshold from the options.
			unset( $mc_settings['free_shipping_threshold'] );
			unset( $mc_settings['offers_free_shipping'] );
			$this->options->update( OptionsInterface::MERCHANT_CENTER, $mc_settings );
		}
	}
}
Migration/Migration20220524T1653383133.php000064400000002160151542112540013107 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\BudgetRecommendationTable;

defined( 'ABSPATH' ) || exit;

/**
 * Class Migration20220524T1653383133
 *
 * Migration class to reload the default Ads budgets recommendations
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.13.3
 */
class Migration20220524T1653383133 extends AbstractMigration {

	/**
	 * @var BudgetRecommendationTable
	 */
	protected $budget_rate_table;

	/**
	 * Migration constructor.
	 *
	 * @param BudgetRecommendationTable $budget_rate_table
	 */
	public function __construct( BudgetRecommendationTable $budget_rate_table ) {
		$this->budget_rate_table = $budget_rate_table;
	}


	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string {
		return '1.13.3';
	}

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void {
		$this->budget_rate_table->reload_data();
	}
}
Migration/Migration20231109T1653383133.php000064400000003542151542112540013115 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\BudgetRecommendationTable;

defined( 'ABSPATH' ) || exit;

/**
 * Class Migration20231109T1653383133
 *
 * Migration class to reload the default Ads budgets recommendations provided by Google on 9 Nov 2023
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 2.5.13
 */
class Migration20231109T1653383133 extends AbstractMigration {

	/**
	 * @var BudgetRecommendationTable
	 */
	protected $budget_rate_table;

	/**
	 * Migration constructor.
	 *
	 * @param \wpdb                     $wpdb
	 * @param BudgetRecommendationTable $budget_rate_table
	 */
	public function __construct( \wpdb $wpdb, BudgetRecommendationTable $budget_rate_table ) {
		parent::__construct( $wpdb );
		$this->budget_rate_table = $budget_rate_table;
	}


	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string {
		return '2.5.13';
	}

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void {
		if ( $this->budget_rate_table->exists() && $this->budget_rate_table->has_column( 'daily_budget_low' ) ) {
			$this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->budget_rate_table->get_name() )}` DROP COLUMN `daily_budget_low`" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		if ( $this->budget_rate_table->exists() && $this->budget_rate_table->has_column( 'daily_budget_high' ) ) {
			$this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->budget_rate_table->get_name() )}` DROP COLUMN `daily_budget_high`" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		}

		$this->budget_rate_table->reload_data();
	}
}
Migration/Migration20240813T1653383133.php000064400000003215151542112540013114 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;

defined( 'ABSPATH' ) || exit;

/**
 * Class Migration20240813T1653383133
 *
 * Migration class to enable min and max time shippings.
 *
 * @see pcTzPl-2qP
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 2.9.1
 */
class Migration20240813T1653383133 extends AbstractMigration {

	/**
	 * @var ShippingTimeTable
	 */
	protected $shipping_time_table;

	/**
	 * Migration constructor.
	 *
	 * @param \wpdb             $wpdb
	 * @param ShippingTimeTable $shipping_time_table
	 */
	public function __construct( \wpdb $wpdb, ShippingTimeTable $shipping_time_table ) {
		parent::__construct( $wpdb );
		$this->shipping_time_table = $shipping_time_table;
	}


	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string {
		return '2.9.1';
	}

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void {
		if ( $this->shipping_time_table->exists() && ! $this->shipping_time_table->has_column( 'max_time' ) ) {
			$this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->shipping_time_table->get_name() )}` Add COLUMN `max_time` bigint(20) NOT NULL default 0" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		// Fill the new column with the current values
		$this->wpdb->query( "UPDATE `{$this->wpdb->_escape( $this->shipping_time_table->get_name() )}` SET `max_time`=`time` WHERE 1=1" );
	}
}
Migration/MigrationInterface.php000064400000001062151542112540012757 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

defined( 'ABSPATH' ) || exit;

/**
 * Interface MigrationInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.4.1
 */
interface MigrationInterface {
	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string;

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void;
}
Migration/MigrationVersion141.php000064400000002546151542112540012742 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class MigrationVersion141
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.4.1
 */
class MigrationVersion141 extends AbstractMigration {

	/**
	 * @var MerchantIssueTable
	 */
	protected $mc_issues_table;

	/**
	 * MigrationVersion141 constructor.
	 *
	 * @param wpdb               $wpdb The wpdb object.
	 * @param MerchantIssueTable $mc_issues_table
	 */
	public function __construct( wpdb $wpdb, MerchantIssueTable $mc_issues_table ) {
		parent::__construct( $wpdb );
		$this->mc_issues_table = $mc_issues_table;
	}


	/**
	 * Returns the version to apply this migration for.
	 *
	 * @return string A version number. For example: 1.4.1
	 */
	public function get_applicable_version(): string {
		return '1.4.1';
	}

	/**
	 * Apply the migrations.
	 *
	 * @return void
	 */
	public function apply(): void {
		if ( $this->mc_issues_table->exists() && $this->mc_issues_table->has_index( 'product_issue' ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->mc_issues_table->get_name() )}` DROP INDEX `product_issue`" );
		}
	}
}
Migration/Migrator.php000064400000003763151542112540011003 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;

defined( 'ABSPATH' ) || exit;

/**
 * Class Migrator
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Migration
 *
 * @since 1.4.1
 */
class Migrator implements Service {

	/**
	 * @var MigrationInterface[]
	 */
	protected $migrations;

	/**
	 * Migrator constructor.
	 *
	 * @param MigrationInterface[] $migrations An array of all available migrations.
	 */
	public function __construct( array $migrations ) {
		$this->migrations = $migrations;

		// Sort migrations by version.
		uasort(
			$this->migrations,
			function ( MigrationInterface $migration_a, MigrationInterface $migration_b ) {
				return version_compare( $migration_a->get_applicable_version(), $migration_b->get_applicable_version() );
			}
		);
	}

	/**
	 * Run migrations.
	 *
	 * @param string $old_version Previous version before updating.
	 * @param string $new_version Current version after updating.
	 */
	public function migrate( string $old_version, string $new_version ): void {
		// bail if both versions are equal.
		if ( 0 === version_compare( $old_version, $new_version ) ) {
			return;
		}

		foreach ( $this->migrations as $migration ) {
			if ( $this->can_apply( $migration->get_applicable_version(), $old_version, $new_version ) ) {
				$migration->apply();
			}
		}
	}

	/**
	 * @param string $migration_version The applicable version of the migration.
	 * @param string $old_version       Previous version before updating.
	 * @param string $new_version       Current version after updating.
	 *
	 * @return bool True if migration should be applied.
	 */
	protected function can_apply( string $migration_version, string $old_version, string $new_version ): bool {
		return version_compare( $old_version, $new_version, '<' ) &&
			version_compare( $old_version, $migration_version, '<' ) &&
			version_compare( $migration_version, $new_version, '<=' );
	}
}
ProductFeedQueryHelper.php000064400000016760151542112540011661 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductMetaHandler;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncer;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use WP_Query;
use WP_REST_Request;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class ProductFeedQueryHelper
 *
 * ContainerAware used to access:
 * - MerchantCenterService
 * - MerchantStatuses
 * - ProductHelper
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
class ProductFeedQueryHelper implements ContainerAwareInterface, Service {

	use ContainerAwareTrait;
	use PluginHelper;

	/**
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * @var WP_REST_Request
	 */
	protected $request;

	/**
	 * @var ProductRepository
	 */
	protected $product_repository;

	/**
	 * Meta key for total sales.
	 */
	protected const META_KEY_TOTAL_SALES = 'total_sales';

	/**
	 * ProductFeedQueryHelper constructor.
	 *
	 * @param wpdb              $wpdb
	 * @param ProductRepository $product_repository
	 */
	public function __construct( wpdb $wpdb, ProductRepository $product_repository ) {
		$this->wpdb               = $wpdb;
		$this->product_repository = $product_repository;
	}

	/**
	 * Retrieve an array of product information using the request params.
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return array
	 *
	 * @throws InvalidValue If the orderby value isn't valid.
	 * @throws Exception If the status data can't be retrieved from Google.
	 */
	public function get( WP_REST_Request $request ): array {
		$this->request           = $request;
		$products                = [];
		$args                    = $this->prepare_query_args();
		$refresh_status_data_job = null;
		list( $limit, $offset )  = $this->prepare_query_pagination();

		$mc_service = $this->container->get( MerchantCenterService::class );
		if ( $mc_service->is_connected() ) {
			$refresh_status_data_job = $this->container->get( MerchantStatuses::class )->maybe_refresh_status_data();
		}

		/** @var ProductHelper $product_helper */
		$product_helper = $this->container->get( ProductHelper::class );

		add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 );

		foreach ( $this->product_repository->find( $args, $limit, $offset ) as $product ) {
			$id        = $product->get_id();
			$errors    = $product_helper->get_validation_errors( $product );
			$mc_status = $product_helper->get_mc_status( $product ) ?: $product_helper->get_sync_status( $product );

			// If the refresh_status_data_job is scheduled, we don't know the status yet as it is being refreshed.
			if ( $refresh_status_data_job && $refresh_status_data_job->is_scheduled() ) {
				$mc_status = null;
			}

			$products[ $id ] = [
				'id'        => $id,
				'title'     => $product->get_name(),
				'visible'   => $product_helper->get_channel_visibility( $product ) !== ChannelVisibility::DONT_SYNC_AND_SHOW,
				'status'    => $mc_status,
				'image_url' => wp_get_attachment_image_url( $product->get_image_id(), 'full' ),
				'price'     => $product->get_price(),
				'errors'    => array_values( $errors ),
			];
		}

		remove_filter( 'posts_where', [ $this, 'title_filter' ] );

		return array_values( $products );
	}

	/**
	 * Count the number of products (using title filter if present).
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return int
	 *
	 * @throws InvalidValue If the orderby value isn't valid.
	 */
	public function count( WP_REST_Request $request ): int {
		$this->request = $request;
		$args          = $this->prepare_query_args();

		add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 );
		$ids = $this->product_repository->find_ids( $args );
		remove_filter( 'posts_where', [ $this, 'title_filter' ] );

		return count( $ids );
	}

	/**
	 * Prepare the args to be used to retrieve the products, namely orderby, meta_query and type.
	 *
	 * @return array
	 *
	 * @throws InvalidValue If the orderby value isn't valid.
	 */
	protected function prepare_query_args(): array {
		$product_types = ProductSyncer::get_supported_product_types();
		$product_types = array_diff( $product_types, [ 'variation' ] );

		$args = [
			'type'    => $product_types,
			'status'  => 'publish',
			'orderby' => [ 'title' => 'ASC' ],
		];

		if ( ! empty( $this->request['ids'] ) ) {
			$args['include'] = explode( ',', $this->request['ids'] );
		}

		if ( ! empty( $this->request['search'] ) ) {
			$args['gla_search'] = $this->request['search'];
		}

		if ( empty( $this->request['orderby'] ) ) {
			return $args;
		}

		switch ( $this->request['orderby'] ) {
			case 'title':
				$args['orderby']['title'] = $this->get_order();
				break;
			case 'id':
				$args['orderby'] = [ 'ID' => $this->get_order() ] + $args['orderby'];
				break;
			case 'visible':
				$args['meta_key'] = $this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY );
				$args['orderby']  = [ 'meta_value' => $this->get_order() ] + $args['orderby'];
				break;
			case 'status':
				$args['meta_key'] = $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS );
				$args['orderby']  = [ 'meta_value' => $this->get_order() ] + $args['orderby'];
				break;
			case 'total_sales':
				$args['meta_key'] = self::META_KEY_TOTAL_SALES;
				$args['orderby']  = [ 'meta_value_num' => $this->get_order() ] + $args['orderby'];
				break;
			default:
				throw InvalidValue::not_in_allowed_list( 'orderby', [ 'title', 'id', 'visible', 'status', 'total_sales' ] );
		}

		return $args;
	}

	/**
	 * Convert the per_page and page parameters into limit and offset values.
	 *
	 * @return array Containing limit and offset values.
	 */
	protected function prepare_query_pagination(): array {
		$limit  = -1;
		$offset = 0;

		if ( ! empty( $this->request['per_page'] ) ) {
			$limit  = intval( $this->request['per_page'] );
			$page   = max( 1, intval( $this->request['page'] ) );
			$offset = $limit * ( $page - 1 );
		}
		return [ $limit, $offset ];
	}

	/**
	 * Filter for the posts_where hook, adds WHERE clause to search
	 * for the 'search' parameter in the product titles (when present).
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
	 *
	 * @return string The updated WHERE clause.
	 */
	public function title_filter( string $where, WP_Query $wp_query ): string {
		$gla_search = $wp_query->get( 'gla_search' );
		if ( $gla_search ) {
			$title_search = '%' . $this->wpdb->esc_like( $gla_search ) . '%';
			$where       .= $this->wpdb->prepare( " AND `{$this->wpdb->posts}`.`post_title` LIKE %s", $title_search ); // phpcs:ignore WordPress.DB.PreparedSQL
		}
		return $where;
	}

	/**
	 * Return the ORDER BY order based on the order request parameter value.
	 *
	 * @return string
	 */
	protected function get_order(): string {
		return strtoupper( $this->request['order'] ?? '' ) === 'DESC' ? 'DESC' : 'ASC';
	}
}
ProductMetaQueryHelper.php000064400000004764151542112540011705 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidMeta;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductMetaHandler;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class ProductMetaQueryHelper.
 *
 * @since 1.1.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
class ProductMetaQueryHelper implements Service {

	use PluginHelper;

	protected const BATCH_SIZE = 500;

	/**
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * ProductMetaQueryHelper constructor.
	 *
	 * @param wpdb $wpdb
	 */
	public function __construct( wpdb $wpdb ) {
		$this->wpdb = $wpdb;
	}

	/**
	 * Get all values for a given meta_key as post_id=>meta_value.
	 *
	 * @param string $meta_key The meta value to retrieve for all posts.
	 * @return array Meta values by post ID.
	 *
	 * @throws InvalidMeta If the meta key isn't valid.
	 */
	public function get_all_values( string $meta_key ): array {
		self::validate_meta_key( $meta_key );

		$query = "SELECT post_id, meta_value FROM {$this->wpdb->postmeta} WHERE meta_key = %s";
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $this->wpdb->get_results( $this->wpdb->prepare( $query, $this->prefix_meta_key( $meta_key ) ) );
		$return  = [];
		foreach ( $results as $r ) {
			$return[ $r->post_id ] = maybe_unserialize( $r->meta_value );
		}
		return $return;
	}

	/**
	 * Delete all values for a given meta_key.
	 *
	 * @since 2.6.4
	 *
	 * @param string $meta_key The meta key to delete.
	 *
	 * @throws InvalidMeta If the meta key isn't valid.
	 */
	public function delete_all_values( string $meta_key ) {
		self::validate_meta_key( $meta_key );
		$meta_key = $this->prefix_meta_key( $meta_key );
		$query    = "DELETE FROM {$this->wpdb->postmeta} WHERE meta_key = %s";
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->query( $this->wpdb->prepare( $query, $meta_key ) );
	}

	/**
	 * @param string $meta_key The meta key to validate
	 *
	 * @throws InvalidMeta If the meta key isn't valid.
	 */
	protected static function validate_meta_key( string $meta_key ) {
		if ( ! ProductMetaHandler::is_meta_key_valid( $meta_key ) ) {
			do_action(
				'woocommerce_gla_error',
				sprintf( 'Product meta key is invalid: %s', $meta_key ),
				__METHOD__
			);

			throw InvalidMeta::invalid_key( $meta_key );
		}
	}
}
Query/AttributeMappingRulesQuery.php000064400000002727151542112540013712 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\AttributeMappingRulesTable;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class defining the queries for the Attribute Mapping Rules Table
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Query
 */
class AttributeMappingRulesQuery extends Query {

	/**
	 * AttributeMappingRulesQuery constructor.
	 *
	 * @param wpdb                       $wpdb
	 * @param AttributeMappingRulesTable $table
	 */
	public function __construct( wpdb $wpdb, AttributeMappingRulesTable $table ) {
		parent::__construct( $wpdb, $table );
	}

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 */
	protected function sanitize_value( string $column, $value ) {
		if ( $column === 'attribute' || $column === 'source' ) {
			return sanitize_text_field( $value );
		}

		if ( $column === 'categories' && is_null( $value ) ) {
			return '';
		}

		return $value;
	}

	/**
	 * Gets a specific rule from Database
	 *
	 * @param int $rule_id The rule ID to get from Database
	 * @return array The rule from database
	 */
	public function get_rule( int $rule_id ): array {
		return $this->where( 'id', $rule_id )->get_row();
	}
}
Query/BudgetRecommendationQuery.php000064400000002355151542112540013514 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\BudgetRecommendationTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class BudgetRecommendationQuery
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Query
 */
class BudgetRecommendationQuery extends Query {

	/**
	 * BudgetRecommendationQuery constructor.
	 *
	 * @param wpdb                      $wpdb
	 * @param BudgetRecommendationTable $table
	 */
	public function __construct( wpdb $wpdb, BudgetRecommendationTable $table ) {
		parent::__construct( $wpdb, $table );
	}

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 * @throws InvalidQuery When the code tries to set the ID column.
	 */
	protected function sanitize_value( string $column, $value ) {
		if ( 'id' === $column ) {
			throw InvalidQuery::cant_set_id( BudgetRecommendationTable::class );
		}

		return $value;
	}
}
Query/MerchantIssueQuery.php000064400000001710151542112540012161 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class MerchantIssueQuery
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Query
 */
class MerchantIssueQuery extends Query {

	/**
	 * MerchantIssueQuery constructor.
	 *
	 * @param wpdb               $wpdb
	 * @param MerchantIssueTable $table
	 */
	public function __construct( wpdb $wpdb, MerchantIssueTable $table ) {
		parent::__construct( $wpdb, $table );
	}

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 */
	protected function sanitize_value( string $column, $value ) {
		return $value;
	}
}
Query/ShippingRateQuery.php000064400000003243151542112540012007 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingRateTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingRateQuery
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Query
 */
class ShippingRateQuery extends Query {

	/**
	 * ShippingRateQuery constructor.
	 *
	 * @param wpdb              $wpdb
	 * @param ShippingRateTable $table
	 */
	public function __construct( wpdb $wpdb, ShippingRateTable $table ) {
		parent::__construct( $wpdb, $table );
	}

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 * @throws InvalidQuery When the code tries to set the ID column.
	 */
	protected function sanitize_value( string $column, $value ) {
		if ( 'id' === $column ) {
			throw InvalidQuery::cant_set_id( ShippingRateTable::class );
		}

		if ( 'options' === $column ) {
			if ( ! is_array( $value ) ) {
				throw InvalidQuery::invalid_value( $column );
			}

			$value = wp_json_encode( $value );
		}

		return $value;
	}

	/**
	 * Perform the query and save it to the results.
	 */
	protected function query_results() {
		parent::query_results();

		$this->results = array_map(
			function ( $row ) {
				$row['options'] = ! empty( $row['options'] ) ? json_decode( $row['options'], true ) : $row['options'];

				return $row;
			},
			$this->results
		);
	}
}
Query/ShippingTimeQuery.php000064400000003126151542112540012012 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingTimeQuery
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Query
 */
class ShippingTimeQuery extends Query {

	/**
	 * ShippingTimeQuery constructor.
	 *
	 * @param wpdb              $wpdb
	 * @param ShippingTimeTable $table
	 */
	public function __construct( wpdb $wpdb, ShippingTimeTable $table ) {
		parent::__construct( $wpdb, $table );
	}

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 * @throws InvalidQuery When the code tries to set the ID column.
	 */
	protected function sanitize_value( string $column, $value ) {
		if ( 'id' === $column ) {
			throw InvalidQuery::cant_set_id( ShippingTimeTable::class );
		}

		return $value;
	}

	/**
	 * Get all shipping times.
	 *
	 * @since 2.8.0
	 *
	 * @return array
	 */
	public function get_all_shipping_times() {
		$times = $this->get_results();
		$items = [];
		foreach ( $times as $time ) {
			$data = [
				'country_code' => $time['country'],
				'time'         => $time['time'],
				'max_time'     => $time['max_time'] ?: $time['time'],
			];

			$items[ $time['country'] ] = $data;
		}

		return $items;
	}
}
Query.php000064400000027112151542112540006365 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\PositiveInteger;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class Query
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
abstract class Query implements QueryInterface {

	/** @var int */
	protected $limit;

	/** @var int */
	protected $offset = 0;

	/** @var array */
	protected $orderby = [];

	/** @var array */
	protected $groupby = [];

	/**
	 * The result of the query.
	 *
	 * @var mixed
	 */
	protected $results = null;

	/**
	 * The number of rows returned by the query.
	 *
	 * @var int
	 */
	protected $count = null;

	/**
	 * The last inserted ID (updated after a call to insert).
	 *
	 * @var int
	 */
	protected $last_insert_id = null;

	/** @var TableInterface */
	protected $table;

	/**
	 * Where clauses for the query.
	 *
	 * @var array
	 */
	protected $where = [];

	/**
	 * Where relation for multiple clauses.
	 *
	 * @var string
	 */
	protected $where_relation;

	/** @var wpdb */
	protected $wpdb;

	/**
	 * Query constructor.
	 *
	 * @param wpdb           $wpdb
	 * @param TableInterface $table
	 */
	public function __construct( wpdb $wpdb, TableInterface $table ) {
		$this->wpdb  = $wpdb;
		$this->table = $table;
	}

	/**
	 * Add a where clause to the query.
	 *
	 * @param string $column  The column name.
	 * @param mixed  $value   The where value.
	 * @param string $compare The comparison to use. Valid values are =, <, >, IN, NOT IN.
	 *
	 * @return $this
	 */
	public function where( string $column, $value, string $compare = '=' ): QueryInterface {
		$this->validate_column( $column );
		$this->validate_compare( $compare );
		$this->where[] = [
			'column'  => $column,
			'value'   => $value,
			'compare' => $compare,
		];

		return $this;
	}

	/**
	 * Add a group by clause to the query.
	 *
	 * @param string $column  The column name.
	 *
	 * @return $this
	 *
	 * @since 1.12.0
	 */
	public function group_by( string $column ): QueryInterface {
		$this->validate_column( $column );
		$this->groupby[] = "`{$column}`";

		return $this;
	}

	/**
	 * Set the where relation for the query.
	 *
	 * @param string $relation
	 *
	 * @return QueryInterface
	 */
	public function set_where_relation( string $relation ): QueryInterface {
		$this->validate_where_relation( $relation );
		$this->where_relation = $relation;

		return $this;
	}

	/**
	 * Set ordering information for the query.
	 *
	 * @param string $column
	 * @param string $order
	 *
	 * @return QueryInterface
	 */
	public function set_order( string $column, string $order = 'ASC' ): QueryInterface {
		$this->validate_column( $column );
		$this->orderby[] = "`{$column}` {$this->normalize_order( $order )}";
		return $this;
	}

	/**
	 * Limit the number of results for the query.
	 *
	 * @param int $limit
	 *
	 * @return QueryInterface
	 */
	public function set_limit( int $limit ): QueryInterface {
		$this->limit = ( new PositiveInteger( $limit ) )->get();

		return $this;
	}

	/**
	 * Set an offset for the results.
	 *
	 * @param int $offset
	 *
	 * @return QueryInterface
	 */
	public function set_offset( int $offset ): QueryInterface {
		$this->offset = ( new PositiveInteger( $offset ) )->get();

		return $this;
	}

	/**
	 * Get the results of the query.
	 *
	 * @return mixed
	 */
	public function get_results() {
		if ( null === $this->results ) {
			$this->query_results();
		}

		return $this->results;
	}

	/**
	 * Get the number of results returned by the query.
	 *
	 * @return int
	 */
	public function get_count(): int {
		if ( null === $this->count ) {
			$this->count_results();
		}

		return $this->count;
	}

	/**
	 * Gets the first result of the query.
	 *
	 * @return array
	 */
	public function get_row(): array {
		if ( null === $this->results ) {
			$old_limit = $this->limit ?? 0;
			$this->set_limit( 1 );
			$this->query_results();
			$this->set_limit( $old_limit );
		}

		return $this->results[0] ?? [];
	}

	/**
	 * Perform the query and save it to the results.
	 */
	protected function query_results() {
		$this->results = $this->wpdb->get_results(
			$this->build_query(), // phpcs:ignore WordPress.DB.PreparedSQL
			ARRAY_A
		);
	}

	/**
	 * Count the results and save the result.
	 */
	protected function count_results() {
		$this->count = (int) $this->wpdb->get_var( $this->build_query( true ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Validate that a given column is valid for the current table.
	 *
	 * @param string $column
	 *
	 * @throws InvalidQuery When the given column is not valid for the current table.
	 */
	protected function validate_column( string $column ) {
		if ( ! array_key_exists( $column, $this->table->get_columns() ) ) {
			throw InvalidQuery::from_column( $column, get_class( $this->table ) );
		}
	}

	/**
	 * Validate that a compare operator is valid.
	 *
	 * @param string $compare
	 *
	 * @throws InvalidQuery When the compare value is not valid.
	 */
	protected function validate_compare( string $compare ) {
		switch ( $compare ) {
			case '=':
			case '>':
			case '<':
			case 'IN':
			case 'NOT IN':
				// These are all valid.
				return;

			default:
				throw InvalidQuery::from_compare( $compare );
		}
	}


	/**
	 * Validate that a where relation is valid.
	 *
	 * @param string $relation
	 *
	 * @throws InvalidQuery When the relation value is not valid.
	 */
	protected function validate_where_relation( string $relation ) {
		switch ( $relation ) {
			case 'AND':
			case 'OR':
				// These are all valid.
				return;

			default:
				throw InvalidQuery::where_relation( $relation );
		}
	}

	/**
	 * Normalize the string for the order.
	 *
	 * Converts the string to uppercase, and will return only DESC or ASC.
	 *
	 * @param string $order
	 *
	 * @return string
	 */
	protected function normalize_order( string $order ): string {
		$order = strtoupper( $order );

		return 'DESC' === $order ? $order : 'ASC';
	}

	/**
	 * Build the query and return the query string.
	 *
	 * @param bool $get_count False to build a normal query, true to build a COUNT(*) query.
	 *
	 * @return string
	 */
	protected function build_query( bool $get_count = false ): string {
		$columns = $get_count ? 'COUNT(*)' : '*';
		$pieces  = [ "SELECT {$columns} FROM `{$this->table->get_name()}`" ];

		$pieces = array_merge( $pieces, $this->generate_where_pieces() );

		if ( ! empty( $this->groupby ) ) {
			$pieces[] = 'GROUP BY ' . implode( ', ', $this->groupby );
		}

		if ( ! $get_count ) {
			if ( $this->orderby ) {
				$pieces[] = 'ORDER BY ' . implode( ', ', $this->orderby );
			}

			if ( $this->limit ) {
				$pieces[] = "LIMIT {$this->limit}";
			}

			if ( $this->offset ) {
				$pieces[] = "OFFSET {$this->offset}";
			}
		}

		return join( "\n", $pieces );
	}

	/**
	 * Generate the pieces for the WHERE part of the query.
	 *
	 * @return string[]
	 */
	protected function generate_where_pieces(): array {
		if ( empty( $this->where ) ) {
			return [];
		}

		$where_pieces = [ 'WHERE' ];
		foreach ( $this->where as $where ) {
			$column  = $where['column'];
			$compare = $where['compare'];

			if ( $compare === 'IN' || $compare === 'NOT IN' ) {
				$value = sprintf(
					"('%s')",
					join(
						"','",
						array_map(
							function ( $value ) {
								return $this->wpdb->_escape( $value );
							},
							$where['value']
						)
					)
				);
			} else {
				$value = "'{$this->wpdb->_escape( $where['value'] )}'";
			}

			if ( count( $where_pieces ) > 1 ) {
				$where_pieces[] = $this->where_relation ?? 'AND';
			}

			$where_pieces[] = "{$column} {$compare} {$value}";
		}

		return $where_pieces;
	}

	/**
	 * Insert a row of data into the table.
	 *
	 * @param array $data
	 *
	 * @return int
	 * @throws InvalidQuery When there is an error inserting the data.
	 */
	public function insert( array $data ): int {
		foreach ( $data as $column => &$value ) {
			$this->validate_column( $column );
			$value = $this->sanitize_value( $column, $value );
		}

		$result = $this->wpdb->insert( $this->table->get_name(), $data );

		if ( false === $result ) {
			throw InvalidQuery::from_insert( $this->wpdb->last_error ?: 'Error inserting data.' );
		}

		// Save a local copy of the last inserted ID.
		$this->last_insert_id = $this->wpdb->insert_id;

		return $result;
	}

	/**
	 * Returns the last inserted ID. Must be called after insert.
	 *
	 * @since 1.12.0
	 *
	 * @return int|null
	 */
	public function last_insert_id(): ?int {
		return $this->last_insert_id;
	}

	/**
	 * Delete rows from the database.
	 *
	 * @param string $where_column Column to use when looking for values to delete.
	 * @param mixed  $value        Value to use when determining what rows to delete.
	 *
	 * @return int The number of rows deleted.
	 * @throws InvalidQuery When there is an error deleting data.
	 */
	public function delete( string $where_column, $value ): int {
		$this->validate_column( $where_column );
		$result = $this->wpdb->delete( $this->table->get_name(), [ $where_column => $value ] );

		if ( false === $result ) {
			throw InvalidQuery::from_delete( $this->wpdb->last_error ?: 'Error deleting data.' );
		}

		return $result;
	}

	/**
	 * Update data in the database.
	 *
	 * @param array $data  Array of columns and their values.
	 * @param array $where Array of where conditions for updating values.
	 *
	 * @return int
	 * @throws InvalidQuery When there is an error updating data, or when an empty where array is provided.
	 */
	public function update( array $data, array $where ): int {
		if ( empty( $where ) ) {
			throw InvalidQuery::empty_where();
		}

		foreach ( $data as $column => &$value ) {
			$this->validate_column( $column );
			$value = $this->sanitize_value( $column, $value );
		}

		$result = $this->wpdb->update(
			$this->table->get_name(),
			$data,
			$where
		);

		if ( false === $result ) {
			throw InvalidQuery::from_update( $this->wpdb->last_error ?: 'Error updating data.' );
		}

		return $result;
	}

	/**
	 * Batch update or insert a set of records.
	 *
	 * @param array $records Array of records to be updated or inserted.
	 *
	 * @throws InvalidQuery If an invalid column name is provided.
	 */
	public function update_or_insert( array $records ): void {
		if ( empty( $records ) ) {
			return;
		}

		$update_values = [];
		$columns       = array_keys( reset( $records ) );
		foreach ( $columns as $c ) {
			$this->validate_column( $c );
			$update_values[] = "`$c`=VALUES(`$c`)";
		}

		$single_placeholder = '(' . implode( ',', array_fill( 0, count( $columns ), "'%s'" ) ) . ')';
		$chunk_size         = 200;
		$num_issues         = count( $records );
		for ( $i = 0; $i < $num_issues; $i += $chunk_size ) {
			$all_values       = [];
			$all_placeholders = [];
			foreach ( array_slice( $records, $i, $chunk_size ) as $issue ) {
				if ( array_keys( $issue ) !== $columns ) {
					throw new InvalidQuery( 'Not all records contain the same columns' );
				}
				$all_placeholders[] = $single_placeholder;
				array_push( $all_values, ...array_values( $issue ) );
			}

			$column_names = '(`' . implode( '`, `', $columns ) . '`)';

			$query  = "INSERT INTO `{$this->table->get_name()}` $column_names VALUES ";
			$query .= implode( ', ', $all_placeholders );
			$query .= ' ON DUPLICATE KEY UPDATE ' . implode( ', ', $update_values );

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

	/**
	 * Sanitize a value for a given column before inserting it into the DB.
	 *
	 * @param string $column The column name.
	 * @param mixed  $value  The value to sanitize.
	 *
	 * @return mixed The sanitized value.
	 */
	abstract protected function sanitize_value( string $column, $value );
}
QueryInterface.php000064400000005776151542112540010222 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidQuery;

defined( 'ABSPATH' ) || exit;

/**
 * Interface QueryInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
interface QueryInterface {

	/**
	 * Set a where clause to query.
	 *
	 * @param string $column  The column name.
	 * @param mixed  $value   The where value.
	 * @param string $compare The comparison to use. Valid values are =, <, >, IN, NOT IN.
	 *
	 * @return $this
	 */
	public function where( string $column, $value, string $compare = '=' ): QueryInterface;

	/**
	 * Set the where relation for the query.
	 *
	 * @param string $relation
	 *
	 * @return QueryInterface
	 */
	public function set_where_relation( string $relation ): QueryInterface;

	/**
	 * @param string $column
	 * @param string $order
	 *
	 * @return QueryInterface
	 */
	public function set_order( string $column, string $order = 'DESC' ): QueryInterface;

	/**
	 * Limit the number of results for the query.
	 *
	 * @param int $limit
	 *
	 * @return QueryInterface
	 */
	public function set_limit( int $limit ): QueryInterface;

	/**
	 * Set an offset for the results.
	 *
	 * @param int $offset
	 *
	 * @return QueryInterface
	 */
	public function set_offset( int $offset ): QueryInterface;

	/**
	 * Get the results of the query.
	 *
	 * @return mixed
	 */
	public function get_results();

	/**
	 * Get the number of results returned by the query.
	 *
	 * @return int
	 */
	public function get_count(): int;

	/**
	 * Gets the first result of the query.
	 *
	 * @return array
	 */
	public function get_row(): array;

	/**
	 * Insert a row of data into the table.
	 *
	 * @param array $data
	 *
	 * @return int
	 * @throws InvalidQuery When there is an error inserting the data.
	 */
	public function insert( array $data ): int;

	/**
	 * Returns the last inserted ID. Must be called after insert.
	 *
	 * @since 1.12.0
	 *
	 * @return int|null
	 */
	public function last_insert_id(): ?int;

	/**
	 * Delete rows from the database.
	 *
	 * @param string $where_column Column to use when looking for values to delete.
	 * @param mixed  $value        Value to use when determining what rows to delete.
	 *
	 * @return int The number of rows deleted.
	 * @throws InvalidQuery When there is an error deleting data.
	 */
	public function delete( string $where_column, $value ): int;

	/**
	 * Update data in the database.
	 *
	 * @param array $data  Array of columns and their values.
	 * @param array $where Array of where conditions for updating values.
	 *
	 * @return int
	 * @throws InvalidQuery When there is an error updating data, or when an empty where array is provided.
	 */
	public function update( array $data, array $where ): int;

	/**
	 * Batch update or insert a set of records.
	 *
	 * @param array $records Array of records to be updated or inserted.
	 *
	 * @throws InvalidQuery If an invalid column name is provided.
	 */
	public function update_or_insert( array $records ): void;
}
Table/AttributeMappingRulesTable.php000064400000002516151542112540013552 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Definition class for the Attribute Mapping Rules Table
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Tables
 */
class AttributeMappingRulesTable extends Table {

	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	protected function get_install_query(): string {
		return "
CREATE TABLE `{$this->get_sql_safe_name()}` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `attribute` varchar(255) NOT NULL,
    `source` varchar(100) NOT NULL,
    `category_condition_type` varchar(10) NOT NULL,
    `categories` text NOT NULL,
    PRIMARY KEY `id` (`id`)
) {$this->get_collation()};
";
	}

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	public static function get_raw_name(): string {
		return 'attribute_mapping_rules';
	}


	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array {
		return [
			'id'                      => true,
			'attribute'               => true,
			'source'                  => true,
			'category_condition_type' => true,
			'categories'              => true,
		];
	}
}
Table/BudgetRecommendationTable.php000064400000007061151542112540013357 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Class BudgetRecommendationTable
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Tables
 */
class BudgetRecommendationTable extends Table {

	/**
	 * Whether the initial data has been loaded
	 *
	 * @var bool
	 */
	public $has_loaded_initial_data = false;

	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	protected function get_install_query(): string {
		return "
CREATE TABLE `{$this->get_sql_safe_name()}` (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    currency varchar(3) NOT NULL,
    country varchar(2) NOT NULL,
    daily_budget int(11) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY country_currency (country, currency)
) {$this->get_collation()};
";
	}

	/**
	 * Install the Database table.
	 *
	 * Add data if there is none.
	 */
	public function install(): void {
		parent::install();

		// Load the data if the table is empty.
		// phpcs:ignore WordPress.DB.PreparedSQL
		$result = $this->wpdb->get_row( "SELECT COUNT(*) AS count FROM `{$this->get_sql_safe_name()}`" );
		if ( empty( $result->count ) ) {
			$this->load_initial_data();
		}
	}

	/**
	 * Reload initial data.
	 *
	 * @return void
	 */
	public function reload_data(): void {
		if ( $this->exists() && ! $this->has_loaded_initial_data ) {
			$this->truncate();
			$this->load_initial_data();
		}
	}

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	public static function get_raw_name(): string {
		return 'budget_recommendations';
	}

	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array {
		return [
			'id'           => true,
			'currency'     => true,
			'country'      => true,
			'daily_budget' => true,
		];
	}

	/**
	 * Load packaged recommendation data on the first install of GLA.
	 *
	 * Inserts 500 records at a time.
	 */
	private function load_initial_data(): void {
		$path       = $this->get_root_dir() . '/data/budget-recommendations.csv';
		$chunk_size = 500;

		if ( file_exists( $path ) ) {
			$csv = array_map(
				function ( $row ) {
					return str_getcsv( $row, ',', '"', '\\' );
				},
				file( $path )
			);

			// Remove the headers
			array_shift( $csv );

			if ( empty( $csv ) ) {
				return;
			}

			$values       = [];
			$placeholders = [];

			// Build placeholders for each row, and add values to data array
			foreach ( $csv as $row ) {

				if ( empty( $row ) ) {
					continue;
				}

				$row_placeholders = [];
				foreach ( $row as $value ) {
					$values[]           = $value;
					$row_placeholders[] = is_numeric( $value ) ? '%d' : '%s';
				}
				$placeholders[] = '(' . implode( ', ', $row_placeholders ) . ')';

				if ( count( $placeholders ) >= $chunk_size ) {
					$this->insert_chunk( $placeholders, $values );
					$placeholders = [];
					$values       = [];
				}
			}

			$this->insert_chunk( $placeholders, $values );
		}

		$this->has_loaded_initial_data = true;
	}

	/**
	 * Insert a chunk of budget recommendations
	 *
	 * @param string[] $placeholders
	 * @param array    $values
	 */
	private function insert_chunk( array $placeholders, array $values ): void {
		$sql  = "INSERT INTO `{$this->get_sql_safe_name()}` (country,daily_budget,currency) VALUES\n";
		$sql .= implode( ",\n", $placeholders );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->query( $this->wpdb->prepare( $sql, $values ) );
	}
}
Table/MerchantIssueTable.php000064400000005620151542112540012031 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;
use DateTime;

defined( 'ABSPATH' ) || exit;

/**
 * Class MerchantIssueTable
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Tables
 */
class MerchantIssueTable extends Table {

	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	protected function get_install_query(): string {
		return "
CREATE TABLE `{$this->get_sql_safe_name()}` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `product_id` bigint(20) NOT NULL,
    `issue` varchar(200) NOT NULL,
    `code` varchar(100) NOT NULL,
    `severity` varchar(20) NOT NULL DEFAULT 'warning',
    `product` varchar(100) NOT NULL,
    `action` text NOT NULL,
    `action_url` varchar(1024) NOT NULL,
    `applicable_countries` text NOT NULL,
    `source` varchar(10) NOT NULL DEFAULT 'mc',
    `type` varchar(10) NOT NULL DEFAULT 'product',
    `created_at` datetime NOT NULL,
    PRIMARY KEY `id` (`id`)
) {$this->get_collation()};
";
	}

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	public static function get_raw_name(): string {
		return 'merchant_issues';
	}

	/**
	 * Delete stale issue records.
	 *
	 * @param DateTime $created_before Delete all records created before this.
	 */
	public function delete_stale( DateTime $created_before ): void {
		$query = "DELETE FROM `{$this->get_sql_safe_name()}` WHERE `created_at` < '%s'";
		$this->wpdb->query( $this->wpdb->prepare( $query, $created_before->format( 'Y-m-d H:i:s' ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Delete product issues for specific products and source.
	 *
	 * @param array  $products_ids Array of product IDs to delete issues for.
	 * @param string $source       The source of the issues. Default is 'mc'.
	 */
	public function delete_specific_product_issues( array $products_ids, string $source = 'mc' ): void {
		if ( empty( $products_ids ) ) {
			return;
		}

		$placeholder = '(' . implode( ',', array_fill( 0, count( $products_ids ), '%d' ) ) . ')';
		$this->wpdb->query( $this->wpdb->prepare( "DELETE FROM `{$this->get_sql_safe_name()}` WHERE `product_id` IN {$placeholder} AND `source` = %s", array_merge( $products_ids, [ $source ] ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array {
		return [
			'id'                   => true,
			'product_id'           => true,
			'code'                 => true,
			'severity'             => true,
			'issue'                => true,
			'product'              => true,
			'action'               => true,
			'action_url'           => true,
			'applicable_countries' => true,
			'source'               => true,
			'type'                 => true,
			'created_at'           => true,
		];
	}
}
Table/ShippingRateTable.php000064400000002347151542112540011657 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingRateTable
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Tables
 */
class ShippingRateTable extends Table {

	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	protected function get_install_query(): string {
		return "
CREATE TABLE `{$this->get_sql_safe_name()}` (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    country varchar(2) NOT NULL,
    currency varchar(3) NOT NULL,
    rate double NOT NULL default 0,
    options text DEFAULT NULL,
    PRIMARY KEY (id),
    KEY country (country),
    KEY currency (currency)
) {$this->get_collation()};
";
	}

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	public static function get_raw_name(): string {
		return 'shipping_rates';
	}

	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array {
		return [
			'id'       => true,
			'country'  => true,
			'currency' => true,
			'rate'     => true,
			'options'  => true,
		];
	}
}
Table/ShippingTimeTable.php000064400000002236151542112540011657 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingTimeTable
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB\Table
 */
class ShippingTimeTable extends Table {

	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	protected function get_install_query(): string {
		return "
CREATE TABLE `{$this->get_sql_safe_name()}` (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    country varchar(2) NOT NULL,
    time bigint(20) NOT NULL default 0,
	max_time bigint(20) NOT NULL default 0,
    PRIMARY KEY (id),
    KEY country (country)
) {$this->get_collation()};
";
	}

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	public static function get_raw_name(): string {
		return 'shipping_times';
	}

	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array {
		return [
			'id'       => true,
			'country'  => true,
			'time'     => true,
			'max_time' => true,
		];
	}
}
Table.php000064400000007744151542112540006320 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use wpdb;

defined( 'ABSPATH' ) || exit;

/**
 * Class Table
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 *
 * @see TableManager::VALID_TABLES contains a list of table classes that will be installed.
 * @see \Automattic\WooCommerce\GoogleListingsAndAds\DB\Installer::install for installing tables.
 */
abstract class Table implements TableInterface {

	use PluginHelper;

	/** @var WP */
	protected $wp;

	/** @var wpdb */
	protected $wpdb;

	/**
	 * Table constructor.
	 *
	 * @param WP   $wp   The WP proxy object.
	 * @param wpdb $wpdb The wpdb object.
	 */
	public function __construct( WP $wp, wpdb $wpdb ) {
		$this->wp   = $wp;
		$this->wpdb = $wpdb;
	}

	/**
	 * Install the Database table.
	 */
	public function install(): void {
		$this->wp->db_delta( $this->get_install_query() );
	}

	/**
	 * Determine whether the table actually exists in the DB.
	 *
	 * @return bool
	 */
	public function exists(): bool {
		$result = $this->wpdb->get_var(
			"SHOW TABLES LIKE '{$this->wpdb->esc_like( $this->get_name() )}'" // phpcs:ignore WordPress.DB.PreparedSQL
		);

		return $result === $this->get_name();
	}

	/**
	 * Delete the Database table.
	 */
	public function delete(): void {
		$this->wpdb->query( "DROP TABLE IF EXISTS `{$this->get_sql_safe_name()}`" ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Truncate the Database table.
	 */
	public function truncate(): void {
		$this->wpdb->query( "TRUNCATE TABLE `{$this->get_sql_safe_name()}`" ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Get the SQL escaped version of the table name.
	 *
	 * @return string
	 */
	protected function get_sql_safe_name(): string {
		return $this->wpdb->_escape( $this->get_name() );
	}

	/**
	 * Get the name of the Database table.
	 *
	 * The name is prefixed with the wpdb prefix, and our plugin prefix.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return "{$this->wpdb->prefix}{$this->get_slug()}_{$this->get_raw_name()}";
	}

	/**
	 * Get the primary column name for the table.
	 *
	 * @return string
	 */
	public function get_primary_column(): string {
		return 'id';
	}

	/**
	 * Checks whether an index exists for the table.
	 *
	 * @param string $index_name The index name.
	 *
	 * @return bool True if the index exists on the table and False if not.
	 *
	 * @since 1.4.1
	 */
	public function has_index( string $index_name ): bool {
		$result = $this->wpdb->get_results(
			$this->wpdb->prepare( "SHOW INDEX FROM `{$this->get_sql_safe_name()}` WHERE Key_name = %s ", [ $index_name ] )  // phpcs:ignore WordPress.DB.PreparedSQL
		);

		return ! empty( $result );
	}

	/**
	 * Get the DB collation.
	 *
	 * @return string
	 */
	protected function get_collation(): string {
		return $this->wpdb->has_cap( 'collation' ) ? $this->wpdb->get_charset_collate() : '';
	}

	/**
	 * Checks whether a column exists for the table.
	 *
	 * @param string $column_name The column name.
	 *
	 * @return bool True if the column exists on the table or False if not.
	 *
	 * @since 2.5.13
	 */
	public function has_column( string $column_name ): bool {
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$this->wpdb->get_results(
			$this->wpdb->prepare( "SHOW COLUMNS FROM `{$this->get_sql_safe_name()}` WHERE Field = %s", [ $column_name ] )
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

		return (bool) $this->wpdb->num_rows;
	}


	/**
	 * Get the schema for the DB.
	 *
	 * This should be a SQL string for creating the DB table.
	 *
	 * @return string
	 */
	abstract protected function get_install_query(): string;

	/**
	 * Get the un-prefixed (raw) table name.
	 *
	 * @return string
	 */
	abstract public static function get_raw_name(): string;
}
TableInterface.php000064400000002717151542112540010134 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

defined( 'ABSPATH' ) || exit;

/**
 * Interface TableInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 */
interface TableInterface {

	/**
	 * Install the Database table.
	 */
	public function install(): void;

	/**
	 * Determine whether the table actually exists in the DB.
	 *
	 * @return bool
	 */
	public function exists(): bool;

	/**
	 * Delete the Database table.
	 */
	public function delete(): void;

	/**
	 * Truncate the Database table.
	 */
	public function truncate(): void;

	/**
	 * Get the name of the Database table.
	 *
	 * @return string
	 */
	public function get_name(): string;

	/**
	 * Get the columns for the table.
	 *
	 * @return array
	 */
	public function get_columns(): array;

	/**
	 * Get the primary column name for the table.
	 *
	 * @return string
	 */
	public function get_primary_column(): string;

	/**
	 * Checks whether an index exists for the table.
	 *
	 * @param string $index_name The index name.
	 *
	 * @return bool True if the index exists on the table and False if not.
	 *
	 * @since 1.4.1
	 */
	public function has_index( string $index_name ): bool;

	/**
	 * Checks whether a column exists for the table.
	 *
	 * @param string $column_name The column name.
	 *
	 * @return bool True if the column exists on the table or False if not.
	 *
	 * @since 2.5.13
	 */
	public function has_column( string $column_name ): bool;
}
TableManager.php000064400000004342151542112540007602 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\DB;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\AttributeMappingRulesTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\BudgetRecommendationTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingRateTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Class TableManager
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\DB
 *
 * @since 1.3.0
 */
class TableManager {

	use ValidateInterface;

	protected const VALID_TABLES = [
		AttributeMappingRulesTable::class => true,
		BudgetRecommendationTable::class  => true,
		MerchantIssueTable::class         => true,
		ShippingRateTable::class          => true,
		ShippingTimeTable::class          => true,
	];

	/**
	 * @var Table[]
	 */
	protected $tables;

	/**
	 * TableManager constructor.
	 *
	 * @param Table[] $tables
	 */
	public function __construct( array $tables ) {
		$this->setup_tables( $tables );
	}

	/**
	 * @return Table[]
	 *
	 * @see \Automattic\WooCommerce\GoogleListingsAndAds\DB\Installer::install for installing these tables.
	 */
	public function get_tables(): array {
		return $this->tables;
	}

	/**
	 * Returns a list of table names to be installed.
	 *
	 * @return string[] Table names
	 *
	 * @see TableManager::VALID_TABLES for the list of valid table classes.
	 */
	public static function get_all_table_names(): array {
		$tables = [];
		foreach ( array_keys( self::VALID_TABLES ) as $table_class ) {
			$table_name = call_user_func( [ $table_class, 'get_raw_name' ] );

			$tables[ $table_name ] = $table_name;
		}

		return $tables;
	}

	/**
	 * Set up each of the table controllers.
	 *
	 * @param Table[] $tables
	 */
	protected function setup_tables( array $tables ) {
		foreach ( $tables as $table ) {
			$this->validate_instanceof( $table, Table::class );

			// only include tables from the installable tables list.
			if ( isset( self::VALID_TABLES[ get_class( $table ) ] ) ) {
				$this->tables[] = $table;
			}
		}
	}
}