rebase on oct-10-2023
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template registry.
|
||||
*/
|
||||
final class BlockTemplateRegistry {
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var BlockTemplateRegistry|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Templates.
|
||||
*/
|
||||
protected $templates = array();
|
||||
|
||||
/**
|
||||
* Get the instance of the class.
|
||||
*/
|
||||
public static function get_instance(): BlockTemplateRegistry {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single template.
|
||||
*
|
||||
* @param string $id Template ID.
|
||||
* @param array $template Template layout.
|
||||
*/
|
||||
public function register( BlockTemplateInterface $template ) {
|
||||
$id = $template->get_id();
|
||||
|
||||
if ( isset( $this->templates[ $id ] ) ) {
|
||||
throw new \ValueError( 'A template with the specified ID already exists in the registry.' );
|
||||
}
|
||||
|
||||
$this->templates[ $id ] = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered templates.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single registered template.
|
||||
*
|
||||
* @param string $id ID of the template
|
||||
*/
|
||||
public function get_registered( $id ): BlockTemplateInterface {
|
||||
return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Block template controller.
|
||||
*/
|
||||
class BlockTemplatesController {
|
||||
|
||||
/**
|
||||
* Block template registry
|
||||
*
|
||||
* @var BlockTemplateRegistry
|
||||
*/
|
||||
private $block_template_registry;
|
||||
|
||||
/**
|
||||
* Block template transformer.
|
||||
*
|
||||
* @var TemplateTransformer
|
||||
*/
|
||||
private $template_transformer;
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init( $block_template_registry, $template_transformer ) {
|
||||
$this->block_template_registry = $block_template_registry;
|
||||
$this->template_transformer = $template_transformer;
|
||||
add_action( 'rest_api_init', array( $this, 'register_templates' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register templates in the blocks endpoint.
|
||||
*/
|
||||
public function register_templates() {
|
||||
$templates = $this->block_template_registry->get_all_registered();
|
||||
|
||||
foreach ( $templates as $template ) {
|
||||
add_filter( 'pre_get_block_templates', function( $query_result, $query, $template_type ) use( $template ) {
|
||||
if ( ! isset( $query['area'] ) || $query['area'] !== $template->get_area() ) {
|
||||
return $query_result;
|
||||
}
|
||||
|
||||
$wp_block_template = $this->template_transformer->transform( $template );
|
||||
$query_result[] = $wp_block_template;
|
||||
|
||||
return $query_result;
|
||||
}, 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Template transformer.
|
||||
*/
|
||||
class TemplateTransformer {
|
||||
|
||||
/**
|
||||
* Transform the WooCommerceBlockTemplate to a WP_Block_Template.
|
||||
*
|
||||
* @param object $block_template The product template.
|
||||
*/
|
||||
public function transform( BlockTemplateInterface $block_template ): \WP_Block_Template {
|
||||
$template = new \WP_Block_Template();
|
||||
$template->id = $block_template->get_id();
|
||||
$template->theme = 'woocommerce/woocommerce';
|
||||
$template->content = $block_template->get_formatted_template();
|
||||
$template->source = 'plugin';
|
||||
$template->slug = $block_template->get_id();
|
||||
$template->type = 'wp_template';
|
||||
$template->title = $block_template->get_title();
|
||||
$template->description = $block_template->get_description();
|
||||
$template->status = 'publish';
|
||||
$template->has_theme_file = true;
|
||||
$template->origin = 'plugin';
|
||||
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
$template->area = $block_template->get_area();
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Block configuration used to specify blocks in BlockTemplate.
|
||||
*/
|
||||
class AbstractBlock implements BlockInterface {
|
||||
/**
|
||||
* The block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* The block ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The block order.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $order = 10;
|
||||
|
||||
/**
|
||||
* The block attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = [];
|
||||
|
||||
/**
|
||||
* The block template that this block belongs to.
|
||||
*
|
||||
* @var BlockTemplate
|
||||
*/
|
||||
private $root_template;
|
||||
|
||||
/**
|
||||
* The parent container.
|
||||
*
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Block constructor.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param BlockContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
*/
|
||||
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
$this->validate( $config, $root_template, $parent );
|
||||
|
||||
$this->root_template = $root_template;
|
||||
$this->parent = is_null( $parent ) ? $root_template : $parent;
|
||||
|
||||
$this->name = $config[ self::NAME_KEY ];
|
||||
|
||||
if ( ! isset( $config[ self::ID_KEY ] ) ) {
|
||||
$this->id = $this->root_template->generate_block_id( $this->get_name() );
|
||||
} else {
|
||||
$this->id = $config[ self::ID_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) ) {
|
||||
$this->order = $config[ self::ORDER_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
$this->attributes = $config[ self::ATTRIBUTES_KEY ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate block configuration.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param ContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
*/
|
||||
protected function validate( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
if ( isset( $parent ) && ( $parent->get_root_template() !== $root_template ) ) {
|
||||
throw new \ValueError( 'The parent block must belong to the same template as the block.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $config[ self::NAME_KEY ] ) || ! is_string( $config[ self::NAME_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block name must be specified.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) && ! is_int( $config[ self::ORDER_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block order must be an integer.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) && ! is_array( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block attributes must be an array.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block order.
|
||||
*/
|
||||
public function get_order(): int {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block order.
|
||||
*
|
||||
* @param int $order The block order.
|
||||
*/
|
||||
public function set_order( int $order ) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block attributes.
|
||||
*/
|
||||
public function get_attributes(): array {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block attributes.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
*/
|
||||
public function set_attributes( array $attributes ) {
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template that this block belongs to.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this->root_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent block container.
|
||||
*/
|
||||
public function &get_parent(): ContainerInterface {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
abstract class AbstractBlockTemplate implements BlockTemplateInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public abstract function get_id(): string;
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template area.
|
||||
*/
|
||||
public function get_area(): string {
|
||||
return 'uncategorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* The block cache.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $block_cache = [];
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*/
|
||||
public function get_block( string $block_id ): ?BlockInterface {
|
||||
return $this->block_cache[ $block_id ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a block in the template. This is an internal method and should not be called directly
|
||||
* except for classes that implement BlockContainerInterface, in their add_block() method.
|
||||
*
|
||||
* @param BlockInterface $block The block to cache.
|
||||
*
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \ValueError If the block template that the block belongs to is not this template.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
public function cache_block( BlockInterface &$block ) {
|
||||
$id = $block->get_id();
|
||||
|
||||
if ( isset( $this->block_cache[ $id ] ) ) {
|
||||
throw new \ValueError( 'A block with the specified ID already exists in the template.' );
|
||||
}
|
||||
|
||||
if ( $block->get_root_template() !== $this ) {
|
||||
throw new \ValueError( 'The block template that the block belongs to must be the same as this template.' );
|
||||
}
|
||||
|
||||
$this->block_cache[ $id ] = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param string $id_base The base to use when generating an ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_block_id( string $id_base ): string {
|
||||
$instance_count = 0;
|
||||
|
||||
do {
|
||||
$instance_count++;
|
||||
$block_id = $id_base . '-' . $instance_count;
|
||||
} while ( isset( $this->block_cache[ $block_id ] ) );
|
||||
|
||||
return $block_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root template.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
$inner_blocks_formatted_template = array_map(
|
||||
function( Block $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
|
||||
return $inner_blocks_formatted_template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Generic block with container properties to be used in BlockTemplate.
|
||||
*/
|
||||
class Block extends AbstractBlock implements BlockContainerInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
if ( ! empty( $inner_blocks ) ) {
|
||||
$arr[] = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inner block to this block.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function &add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
|
||||
/**
|
||||
* Trait for block containers.
|
||||
*/
|
||||
trait BlockContainerTrait {
|
||||
/**
|
||||
* The inner blocks.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $inner_blocks = [];
|
||||
|
||||
// phpcs doesn't take into account exceptions thrown by called methods.
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Add a block to the block container.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \UnexpectedValueException If the block container is not the parent of the block.
|
||||
*/
|
||||
protected function &add_inner_block( BlockInterface $block ): BlockInterface {
|
||||
if ( ! $block instanceof BlockInterface ) {
|
||||
throw new \UnexpectedValueException( 'The block must return an instance of BlockInterface.' );
|
||||
}
|
||||
|
||||
if ( $block->get_parent() !== $this ) {
|
||||
throw new \UnexpectedValueException( 'The block container is not the parent of the block.' );
|
||||
}
|
||||
|
||||
$root_template = $block->get_root_template();
|
||||
$root_template->cache_block( $block );
|
||||
$this->inner_blocks[] = &$block;
|
||||
return $block;
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Get the inner blocks sorted by order.
|
||||
*/
|
||||
private function get_inner_blocks_sorted_by_order(): array {
|
||||
$sorted_inner_blocks = $this->inner_blocks;
|
||||
|
||||
usort(
|
||||
$sorted_inner_blocks,
|
||||
function( Block $a, Block $b ) {
|
||||
return $a->get_order() <=> $b->get_order();
|
||||
}
|
||||
);
|
||||
|
||||
return $sorted_inner_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
if ( ! empty( $inner_blocks ) ) {
|
||||
$arr[] = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
class BlockTemplate extends AbstractBlockTemplate {
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return 'woocommerce-block-template';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
|
||||
use Automattic\WooCommerce\Internal\Admin\Settings;
|
||||
@@ -72,6 +73,7 @@ class Loader {
|
||||
|
||||
wc_get_container()->get( Reviews::class );
|
||||
wc_get_container()->get( ReviewsCommentsOverrides::class );
|
||||
wc_get_container()->get( BlockTemplatesController::class );
|
||||
|
||||
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
|
||||
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
|
||||
@@ -302,7 +304,7 @@ class Loader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks extra neccessary data into the component settings array already set in WooCommerce core.
|
||||
* Hooks extra necessary data into the component settings array already set in WooCommerce core.
|
||||
*
|
||||
* @param array $settings Array of component settings.
|
||||
* @return array Array of component settings.
|
||||
|
||||
@@ -52,7 +52,7 @@ class MarketingSpecs {
|
||||
|
||||
if ( false === $plugins ) {
|
||||
$request = wp_remote_get(
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.2/recommendations.json',
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json',
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
@@ -136,34 +136,41 @@ class MarketingSpecs {
|
||||
/**
|
||||
* Load knowledge base posts from WooCommerce.com
|
||||
*
|
||||
* @param string|null $category Category of posts to retrieve.
|
||||
* @param string|null $term Term of posts to retrieve.
|
||||
* @return array
|
||||
*/
|
||||
public function get_knowledge_base_posts( ?string $category ): array {
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT;
|
||||
|
||||
$categories = array(
|
||||
'marketing' => 1744,
|
||||
'coupons' => 25202,
|
||||
public function get_knowledge_base_posts( ?string $term ): array {
|
||||
$terms = array(
|
||||
'marketing' => array(
|
||||
'taxonomy' => 'category',
|
||||
'term_id' => 1744,
|
||||
'argument' => 'categories',
|
||||
),
|
||||
'coupons' => array(
|
||||
'taxonomy' => 'post_tag',
|
||||
'term_id' => 1377,
|
||||
'argument' => 'tags',
|
||||
),
|
||||
);
|
||||
|
||||
// Default to marketing category (if no category set on the kb component).
|
||||
if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) {
|
||||
$category_id = $categories[ $category ];
|
||||
$kb_transient = $kb_transient . '_' . strtolower( $category );
|
||||
} else {
|
||||
$category_id = $categories['marketing'];
|
||||
// Default to the marketing category (if no term is set on the kb component).
|
||||
if ( empty( $term ) || ! array_key_exists( $term, $terms ) ) {
|
||||
$term = 'marketing';
|
||||
}
|
||||
|
||||
$term_id = $terms[ $term ]['term_id'];
|
||||
$argument = $terms[ $term ]['argument'];
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT . '_' . strtolower( $term );
|
||||
|
||||
$posts = get_transient( $kb_transient );
|
||||
|
||||
if ( false === $posts ) {
|
||||
$request_url = add_query_arg(
|
||||
array(
|
||||
'categories' => $category_id,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
$argument => $term_id,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
),
|
||||
'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product'
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ class CouponPageMoved {
|
||||
|
||||
add_action( 'admin_init', [ $this, 'possibly_add_note' ] );
|
||||
add_action( 'admin_init', [ $this, 'redirect_to_coupons' ] );
|
||||
add_action( 'woocommerce_admin_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
|
||||
add_action( 'woocommerce_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -38,19 +38,13 @@ class PaymentsMoreInfoNeeded {
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// If user has installed WCPay, don't show this note.
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User has dismissed the WCPay Welcome Page.
|
||||
if ( 'yes' !== get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
|
||||
// WCPay welcome page must not be visible.
|
||||
if ( WcPayWelcomePage::instance()->must_be_visible() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// More than 30 days since viewing the welcome page.
|
||||
$exit_survey_timestamp = get_option( 'wc_pay_exit_survey_more_info_needed_timestamp', false );
|
||||
$exit_survey_timestamp = get_option( 'wcpay_welcome_page_exit_survey_more_info_needed_timestamp', false );
|
||||
if ( ! $exit_survey_timestamp ||
|
||||
( time() - $exit_survey_timestamp < 30 * DAY_IN_SECONDS )
|
||||
) {
|
||||
@@ -69,10 +63,10 @@ class PaymentsMoreInfoNeeded {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$content = __( 'We recently asked you if you wanted more information about WooCommerce Payments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' );
|
||||
$content = __( 'We recently asked you if you wanted more information about WooPayments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Payments made simple with WooCommerce Payments', 'woocommerce' ) );
|
||||
$note->set_title( __( 'Payments made simple with WooPayments', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -38,18 +38,13 @@ class PaymentsRemindMeLater {
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// Installed WCPay.
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
|
||||
return false;
|
||||
}
|
||||
// Dismissed WCPay welcome page.
|
||||
if ( 'yes' === get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
|
||||
// WCPay welcome page must be visible.
|
||||
if ( ! WcPayWelcomePage::instance()->must_be_visible() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Less than 3 days since viewing welcome page.
|
||||
$view_timestamp = get_option( 'wc_pay_welcome_page_viewed_timestamp', false );
|
||||
$view_timestamp = get_option( 'wcpay_welcome_page_viewed_timestamp', false );
|
||||
if ( ! $view_timestamp ||
|
||||
( time() - $view_timestamp < 3 * DAY_IN_SECONDS )
|
||||
) {
|
||||
@@ -68,10 +63,10 @@ class PaymentsRemindMeLater {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$content = __( 'Save up to $800 in fees by managing transactions with WooCommerce Payments. With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' );
|
||||
$content = __( 'Save up to $800 in fees by managing transactions with WooPayments. With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Save big with WooCommerce Payments', 'woocommerce' ) );
|
||||
$note->set_title( __( 'Save big with WooPayments', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
|
||||
@@ -105,7 +105,7 @@ class WooCommercePayments {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Try the new way to get paid', 'woocommerce' ) );
|
||||
$note->set_content(
|
||||
__( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooCommerce Payments</strong>.', 'woocommerce' ) .
|
||||
__( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooPayments</strong>.', 'woocommerce' ) .
|
||||
'<br><br>' .
|
||||
sprintf(
|
||||
/* translators: 1: opening link tag, 2: closing tag */
|
||||
|
||||
@@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Woo_Subscriptions_Notes
|
||||
@@ -36,7 +37,7 @@ class WooSubscriptionsNotes {
|
||||
* Hook all the things.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_init', array( $this, 'admin_init' ) );
|
||||
add_action( 'admin_head', array( $this, 'admin_head' ) );
|
||||
add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
@@ -75,9 +76,16 @@ class WooSubscriptionsNotes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Things to do on admin_init.
|
||||
* Runs on `admin_head` hook. Checks the connection and refreshes subscription notes on relevant pages.
|
||||
*/
|
||||
public function admin_init() {
|
||||
public function admin_head() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
// To avoid unnecessarily calling Helper API, we only want to refresh subscription notes,
|
||||
// if the request is initiated from the wc admin dashboard or a WC related page which includes
|
||||
// the Activity button in WC header.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->check_connection();
|
||||
|
||||
if ( $this->is_connected() ) {
|
||||
|
||||
@@ -128,9 +128,6 @@ class OnboardingProducts {
|
||||
$product_data[ $key ]['description'] = $products[ $product_type['product'] ]->excerpt;
|
||||
$product_data[ $key ]['more_url'] = $products[ $product_type['product'] ]->link;
|
||||
$product_data[ $key ]['slug'] = strtolower( preg_replace( '~[^\pL\d]+~u', '-', $products[ $product_type['product'] ]->slug ) );
|
||||
} elseif ( isset( $product_type['product'] ) ) {
|
||||
/* translators: site currency symbol (used to show that the product costs money) */
|
||||
$product_data[ $key ]['label'] .= sprintf( __( ' — %s', 'woocommerce' ), html_entity_decode( get_woocommerce_currency_symbol() ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ class OnboardingProfile {
|
||||
* Add onboarding actions.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_updated', array( __CLASS__, 'maybe_mark_complete' ) );
|
||||
add_action( 'update_option_' . self::DATA_OPTION, array( __CLASS__, 'trigger_complete' ), 10, 2 );
|
||||
}
|
||||
|
||||
@@ -65,37 +64,4 @@ class OnboardingProfile {
|
||||
// https://github.com/woocommerce/woocommerce-admin/pull/2300#discussion_r287237498.
|
||||
return ! $is_completed && ! $is_skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* When updating WooCommerce, mark the profiler and task list complete.
|
||||
*
|
||||
* @todo The `maybe_enable_setup_wizard()` method should be revamped on onboarding enable in core.
|
||||
* See https://github.com/woocommerce/woocommerce/blob/1ca791f8f2325fe2ee0947b9c47e6a4627366374/includes/class-wc-install.php#L341
|
||||
*/
|
||||
public static function maybe_mark_complete() {
|
||||
// The install notice still exists so don't complete the profiler.
|
||||
if ( ! class_exists( 'WC_Admin_Notices' ) || \WC_Admin_Notices::has_notice( 'install' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onboarding_data = get_option( self::DATA_OPTION, array() );
|
||||
// Don't make updates if the profiler is completed or skipped, but task list is potentially incomplete.
|
||||
if (
|
||||
( isset( $onboarding_data['completed'] ) && $onboarding_data['completed'] ) ||
|
||||
( isset( $onboarding_data['skipped'] ) && $onboarding_data['skipped'] )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onboarding_data['completed'] = true;
|
||||
update_option( self::DATA_OPTION, $onboarding_data );
|
||||
|
||||
if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS ) ) {
|
||||
$task_list = TaskLists::get_list( 'setup' );
|
||||
if ( ! $task_list ) {
|
||||
return;
|
||||
}
|
||||
$task_list->hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ class COTRedirectionController {
|
||||
$params['_wpnonce'] = wp_create_nonce( 'bulk-posts' );
|
||||
}
|
||||
|
||||
// If an `order` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['order'] ) && is_array( $params['order'] ) ) {
|
||||
$params['post'] = $params['order'];
|
||||
unset( $params['order'] );
|
||||
// If an `id` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['id'] ) && is_array( $params['id'] ) ) {
|
||||
$params['post'] = $params['id'];
|
||||
unset( $params['id'] );
|
||||
}
|
||||
|
||||
$params['post_type'] = 'shop_order';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
|
||||
/**
|
||||
* Class Edit.
|
||||
@@ -26,6 +27,13 @@ class Edit {
|
||||
*/
|
||||
private $custom_meta_box;
|
||||
|
||||
/**
|
||||
* Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies.
|
||||
*
|
||||
* @var TaxonomiesMetaBox
|
||||
*/
|
||||
private $taxonomies_meta_box;
|
||||
|
||||
/**
|
||||
* Instance of WC_Order to be used in metaboxes.
|
||||
*
|
||||
@@ -47,6 +55,13 @@ class Edit {
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* Controller for orders page. Used to determine redirection URLs.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $orders_page_controller;
|
||||
|
||||
/**
|
||||
* Hooks all meta-boxes for order edit page. This is static since this may be called by post edit form rendering.
|
||||
*
|
||||
@@ -96,6 +111,20 @@ class Edit {
|
||||
wp_enqueue_script( 'post' ); // Ensure existing JS libraries are still available for backward compat.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PageController for this edit form. This method is protected to allow child classes to overwrite the PageController object and return custom links.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*
|
||||
* @return PageController PageController object.
|
||||
*/
|
||||
protected function get_page_controller() {
|
||||
if ( ! isset( $this->orders_page_controller ) ) {
|
||||
$this->orders_page_controller = wc_get_container()->get( PageController::class );
|
||||
}
|
||||
return $this->orders_page_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup hooks, actions and variables needed to render order edit page.
|
||||
*
|
||||
@@ -103,17 +132,22 @@ class Edit {
|
||||
*/
|
||||
public function setup( \WC_Order $order ) {
|
||||
$this->order = $order;
|
||||
$wc_screen_id = wc_get_page_screen_id( 'shop-order' );
|
||||
$current_screen = get_current_screen();
|
||||
$current_screen->is_block_editor( false );
|
||||
$this->screen_id = $current_screen->id;
|
||||
if ( ! isset( $this->custom_meta_box ) ) {
|
||||
$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->taxonomies_meta_box ) ) {
|
||||
$this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class );
|
||||
}
|
||||
|
||||
$this->add_save_meta_boxes();
|
||||
$this->handle_order_update();
|
||||
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
|
||||
$this->add_order_specific_meta_box();
|
||||
$this->add_order_taxonomies_meta_box();
|
||||
|
||||
/**
|
||||
* From wp-admin/includes/meta-boxes.php.
|
||||
@@ -122,7 +156,7 @@ class Edit {
|
||||
*
|
||||
* @since 3.8.0.
|
||||
*/
|
||||
do_action( 'add_meta_boxes', $wc_screen_id, $this->order );
|
||||
do_action( 'add_meta_boxes', $this->screen_id, $this->order );
|
||||
|
||||
/**
|
||||
* Provides an opportunity to inject custom meta boxes into the order editor screen. This
|
||||
@@ -132,7 +166,7 @@ class Edit {
|
||||
*
|
||||
* @oaram WC_Order $order The order being edited.
|
||||
*/
|
||||
do_action( 'add_meta_boxes_' . $wc_screen_id, $this->order );
|
||||
do_action( 'add_meta_boxes_' . $this->screen_id, $this->order );
|
||||
|
||||
$this->enqueue_scripts();
|
||||
}
|
||||
@@ -159,6 +193,15 @@ class Edit {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom meta box.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_order_taxonomies_meta_box() {
|
||||
$this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
|
||||
*
|
||||
@@ -176,6 +219,10 @@ class Edit {
|
||||
|
||||
check_admin_referer( $this->get_order_edit_nonce_action() );
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object.
|
||||
$taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null;
|
||||
$this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input );
|
||||
|
||||
/**
|
||||
* Save meta for shop order.
|
||||
*
|
||||
@@ -189,9 +236,39 @@ class Edit {
|
||||
// Order updated message.
|
||||
$this->message = 1;
|
||||
|
||||
// Refresh the order from DB.
|
||||
$this->order = wc_get_order( $this->order->get_id() );
|
||||
$theorder = $this->order;
|
||||
$this->redirect_order( $this->order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to redirect to order edit page.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
private function redirect_order( \WC_Order $order ) {
|
||||
$redirect_to = $this->get_page_controller()->get_edit_url( $order->get_id() );
|
||||
if ( isset( $this->message ) ) {
|
||||
$redirect_to = add_query_arg( 'message', $this->message, $redirect_to );
|
||||
}
|
||||
wp_safe_redirect(
|
||||
/**
|
||||
* Filter the URL used to redirect after an order is updated. Similar to the WP post's `redirect_post_location` filter.
|
||||
*
|
||||
* @param string $redirect_to The redirect destination URL.
|
||||
* @param int $order_id The order ID.
|
||||
* @param \WC_Order $order The order object.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
apply_filters(
|
||||
'woocommerce_redirect_order_location',
|
||||
$redirect_to,
|
||||
$order->get_id(),
|
||||
$order
|
||||
)
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,10 +330,10 @@ class Edit {
|
||||
private function render_wrapper_start( $notice = '', $message = '' ) {
|
||||
$post_type = get_post_type_object( $this->order->get_type() );
|
||||
|
||||
$edit_page_url = wc_get_container()->get( PageController::class )->get_edit_url( $this->order->get_id() );
|
||||
$edit_page_url = $this->get_page_controller()->get_edit_url( $this->order->get_id() );
|
||||
$form_action = 'edit_order';
|
||||
$referer = wp_get_referer();
|
||||
$new_page_url = wc_get_container()->get( PageController::class )->get_new_page_url( $this->order->get_type() );
|
||||
$new_page_url = $this->get_page_controller()->get_new_page_url( $this->order->get_type() );
|
||||
|
||||
?>
|
||||
<div class="wrap">
|
||||
@@ -298,9 +375,20 @@ class Edit {
|
||||
?>
|
||||
>
|
||||
<?php wp_nonce_field( $this->get_order_edit_nonce_action() ); ?>
|
||||
<?php
|
||||
/**
|
||||
* Fires at the top of the order edit form. Can be used as a replacement for edit_form_top hook for HPOS.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
do_action( 'order_edit_form_top', $this->order );
|
||||
?>
|
||||
<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
|
||||
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
|
||||
<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
|
||||
<input type="hidden" id="post_ID" name="post_ID" value="<?php echo esc_attr( $this->order->get_id() ); ?>"/>
|
||||
<div id="poststuff">
|
||||
<div id="post-body"
|
||||
class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
/**
|
||||
* This class takes care of the edit lock logic when HPOS is enabled.
|
||||
* For better interoperability with WordPress, edit locks are stored in the same format as posts. That is, as a metadata
|
||||
* in the order object (key: '_edit_lock') in the format "timestamp:user_id".
|
||||
*
|
||||
* @since 7.8.0
|
||||
*/
|
||||
class EditLock {
|
||||
|
||||
const META_KEY_NAME = '_edit_lock';
|
||||
|
||||
/**
|
||||
* Obtains lock information for a given order. If the lock has expired or it's assigned to an invalid user,
|
||||
* the order is no longer considered locked.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return bool|array
|
||||
*/
|
||||
public function get_lock( \WC_Order $order ) {
|
||||
$lock = $order->get_meta( self::META_KEY_NAME, true, 'edit' );
|
||||
if ( ! $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = explode( ':', $lock );
|
||||
if ( 2 !== count( $lock ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$time = absint( $lock[0] );
|
||||
$user_id = isset( $lock[1] ) ? absint( $lock[1] ) : 0;
|
||||
|
||||
if ( ! $time || ! get_user_by( 'id', $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** This filter is documented in WP's wp-admin/includes/ajax-actions.php */
|
||||
$time_window = apply_filters( 'wp_check_post_lock_window', 150 ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
if ( time() >= ( $time + $time_window ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compact( 'time', 'user_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the order is being edited (i.e. locked) by another user.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return bool TRUE if order is locked and currently being edited by another user. FALSE otherwise.
|
||||
*/
|
||||
public function is_locked_by_another_user( \WC_Order $order ) : bool {
|
||||
$lock = $this->get_lock( $order );
|
||||
return $lock && ( get_current_user_id() !== $lock['user_id'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the order is being edited by any user.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return boolean TRUE if order is locked and currently being edited by a user. FALSE otherwise.
|
||||
*/
|
||||
public function is_locked( \WC_Order $order ) : bool {
|
||||
return (bool) $this->get_lock( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns an order's edit lock to the current user.
|
||||
*
|
||||
* @param \WC_Order $order The order to apply the lock to.
|
||||
* @return array|bool FALSE if no user is logged-in, an array in the same format as {@see get_lock()} otherwise.
|
||||
*/
|
||||
public function lock( \WC_Order $order ) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->update_meta_data( self::META_KEY_NAME, time() . ':' . $user_id );
|
||||
$order->save_meta_data();
|
||||
|
||||
return $order->get_meta( self::META_KEY_NAME, true, 'edit' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked to 'heartbeat_received' on the edit order page to refresh the lock on an order being edited by the current user.
|
||||
*
|
||||
* @param array $response The heartbeat response to be sent.
|
||||
* @param array $data Data sent through the heartbeat.
|
||||
* @return array Response to be sent.
|
||||
*/
|
||||
public function refresh_lock_ajax( $response, $data ) {
|
||||
$order_id = absint( $data['wc-refresh-order-lock'] ?? 0 );
|
||||
if ( ! $order_id ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response['wc-refresh-order-lock'] = array();
|
||||
|
||||
if ( ! $this->is_locked_by_another_user( $order ) ) {
|
||||
$response['wc-refresh-order-lock']['lock'] = $this->lock( $order );
|
||||
} else {
|
||||
$current_lock = $this->get_lock( $order );
|
||||
$user = get_user_by( 'id', $current_lock['user_id'] );
|
||||
|
||||
$response['wc-refresh-order-lock']['error'] = array(
|
||||
// translators: %s is a user's name.
|
||||
'message' => sprintf( __( '%s has taken over and is currently editing.', 'woocommerce' ), $user->display_name ),
|
||||
'user_name' => $user->display_name,
|
||||
'user_avatar_src' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 64 ) ) : '',
|
||||
'user_avatar_src_2x' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 128 ) ) : '',
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked to 'heartbeat_received' on the orders screen to refresh the locked status of orders in the list table.
|
||||
*
|
||||
* @param array $response The heartbeat response to be sent.
|
||||
* @param array $data Data sent through the heartbeat.
|
||||
* @return array Response to be sent.
|
||||
*/
|
||||
public function check_locked_orders_ajax( $response, $data ) {
|
||||
if ( empty( $data['wc-check-locked-orders'] ) || ! is_array( $data['wc-check-locked-orders'] ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response['wc-check-locked-orders'] = array();
|
||||
|
||||
$order_ids = array_unique( array_map( 'absint', $data['wc-check-locked-orders'] ) );
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $this->is_locked_by_another_user( $order ) || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response['wc-check-locked-orders'][ $order_id ] = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs HTML for the lock dialog based on the status of the lock on the order (if any).
|
||||
* Depending on who owns the lock, this could be a message with the chance to take over or a message indicating that
|
||||
* someone else has taken over the order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_dialog( $order ) {
|
||||
$locked = $this->is_locked_by_another_user( $order );
|
||||
$lock = $this->get_lock( $order );
|
||||
$user = get_user_by( 'id', $lock['user_id'] );
|
||||
|
||||
$edit_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_edit_url( $order->get_id() );
|
||||
|
||||
$sendback_url = wp_get_referer();
|
||||
if ( ! $sendback_url ) {
|
||||
$sendback_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_base_page_url( $order->get_type() );
|
||||
}
|
||||
|
||||
$sendback_text = __( 'Go back', 'woocommerce' );
|
||||
?>
|
||||
<div id="post-lock-dialog" class="notification-dialog-wrap <?php echo $locked ? '' : 'hidden'; ?> order-lock-dialog">
|
||||
<div class="notification-dialog-background"></div>
|
||||
<div class="notification-dialog">
|
||||
<?php if ( $locked ) : ?>
|
||||
<div class="post-locked-message">
|
||||
<div class="post-locked-avatar"><?php echo get_avatar( $user->ID, 64 ); ?></div>
|
||||
<p class="currently-editing wp-tab-first" tabindex="0">
|
||||
<?php
|
||||
// translators: %s is a user's name.
|
||||
echo esc_html( sprintf( __( '%s is currently editing this order. Do you want to take over?', 'woocommerce' ), esc_html( $user->display_name ) ) );
|
||||
?>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a>
|
||||
<a class="button button-primary wp-tab-last" href="<?php echo esc_url( add_query_arg( 'claim-lock', '1', wp_nonce_url( $edit_url, 'claim-lock-' . $order->get_id() ) ) ); ?>"><?php esc_html_e( 'Take over', 'woocommerce' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="post-taken-over">
|
||||
<div class="post-locked-avatar"></div>
|
||||
<p class="wp-tab-first" tabindex="0">
|
||||
<span class="currently-editing"></span><br />
|
||||
</p>
|
||||
<p><a class="button button-primary wp-tab-last" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
use WP_List_Table;
|
||||
use WP_Screen;
|
||||
@@ -109,6 +110,44 @@ class ListTable extends WP_List_Table {
|
||||
add_action( 'manage_' . wc_get_page_screen_id( $this->order_type ) . '_custom_column', array( $this, 'render_column' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates content for a single row of the table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param \WC_Order $order The current order.
|
||||
*/
|
||||
public function single_row( $order ) {
|
||||
/**
|
||||
* Filters the list of CSS class names for a given order row in the orders list table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param string[] $classes An array of CSS class names.
|
||||
* @param \WC_Order $order The order object.
|
||||
*/
|
||||
$css_classes = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_order_css_classes',
|
||||
array(
|
||||
'order-' . $order->get_id(),
|
||||
'type-' . $order->get_type(),
|
||||
'status-' . $order->get_status(),
|
||||
),
|
||||
$order
|
||||
);
|
||||
$css_classes = array_unique( array_map( 'trim', $css_classes ) );
|
||||
|
||||
// Is locked?
|
||||
$edit_lock = wc_get_container()->get( EditLock::class );
|
||||
if ( $edit_lock->is_locked_by_another_user( $order ) ) {
|
||||
$css_classes[] = 'wp-locked';
|
||||
}
|
||||
|
||||
echo '<tr id="order-' . esc_attr( $order->get_id() ) . '" class="' . esc_attr( implode( ' ', $css_classes ) ) . '">';
|
||||
$this->single_row_columns( $order );
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual column.
|
||||
*
|
||||
@@ -276,6 +315,37 @@ class ListTable extends WP_List_Table {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of CSS classes for the WP_List_Table table tag.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @return string[] Array of CSS classes for the table tag.
|
||||
*/
|
||||
protected function get_table_classes() {
|
||||
/**
|
||||
* Filters the list of CSS class names for the orders list table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param string[] $classes An array of CSS class names.
|
||||
* @param string $order_type The order type.
|
||||
*/
|
||||
$css_classes = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_css_classes',
|
||||
array_merge(
|
||||
parent::get_table_classes(),
|
||||
array(
|
||||
'wc-orders-list-table',
|
||||
'wc-orders-list-table-' . $this->order_type,
|
||||
)
|
||||
),
|
||||
$this->order_type
|
||||
);
|
||||
|
||||
return array_unique( array_map( 'trim', $css_classes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*/
|
||||
@@ -471,7 +541,7 @@ class ListTable extends WP_List_Table {
|
||||
$view_counts[ $slug ] = $total_in_status;
|
||||
}
|
||||
|
||||
if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list ) {
|
||||
if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list && 'auto-draft' !== $slug ) {
|
||||
$all_count += $total_in_status;
|
||||
}
|
||||
}
|
||||
@@ -550,8 +620,9 @@ class ListTable extends WP_List_Table {
|
||||
array_merge(
|
||||
wc_get_order_statuses(),
|
||||
array(
|
||||
'trash' => ( get_post_status_object( 'trash' ) )->label,
|
||||
'draft' => ( get_post_status_object( 'draft' ) )->label,
|
||||
'trash' => ( get_post_status_object( 'trash' ) )->label,
|
||||
'draft' => ( get_post_status_object( 'draft' ) )->label,
|
||||
'auto-draft' => ( get_post_status_object( 'auto-draft' ) )->label,
|
||||
)
|
||||
),
|
||||
array_flip( get_post_stati( array( 'show_in_admin_status_list' => true ) ) )
|
||||
@@ -794,7 +865,21 @@ class ListTable extends WP_List_Table {
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ) {
|
||||
return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', esc_attr( $this->_args['singular'] ), esc_attr( $item->get_id() ) );
|
||||
ob_start();
|
||||
?>
|
||||
<input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="id[]" value="<?php echo esc_attr( $item->get_id() ); ?>" />
|
||||
|
||||
<div class="locked-indicator">
|
||||
<span class="locked-indicator-icon" aria-hidden="true"></span>
|
||||
<span class="screen-reader-text">
|
||||
<?php
|
||||
// translators: %s is an order ID.
|
||||
echo esc_html( sprintf( __( 'Order %s is locked.', 'woocommerce' ), $item->get_id() ) );
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,7 +1001,7 @@ class ListTable extends WP_List_Table {
|
||||
}
|
||||
|
||||
// Gracefully handle legacy statuses.
|
||||
if ( in_array( $order->get_status(), array( 'trash', 'draft' ), true ) ) {
|
||||
if ( in_array( $order->get_status(), array( 'trash', 'draft', 'auto-draft' ), true ) ) {
|
||||
$status_name = ( get_post_status_object( $order->get_status() ) )->label;
|
||||
} else {
|
||||
$status_name = wc_get_order_status_name( $order->get_status() );
|
||||
@@ -1112,7 +1197,7 @@ class ListTable extends WP_List_Table {
|
||||
|
||||
$action = 'delete';
|
||||
} else {
|
||||
$ids = isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', $_REQUEST['order'] ) ) : array();
|
||||
$ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1132,8 +1217,9 @@ class ListTable extends WP_List_Table {
|
||||
exit;
|
||||
}
|
||||
|
||||
$report_action = '';
|
||||
$changed = 0;
|
||||
$report_action = '';
|
||||
$changed = 0;
|
||||
$action_handled = true;
|
||||
|
||||
if ( 'remove_personal_data' === $action ) {
|
||||
$report_action = 'removed_personal_data';
|
||||
@@ -1154,8 +1240,15 @@ class ListTable extends WP_List_Table {
|
||||
|
||||
if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) {
|
||||
$changed = $this->do_bulk_action_mark_orders( $ids, $new_status );
|
||||
} else {
|
||||
$action_handled = false;
|
||||
}
|
||||
} else {
|
||||
$action_handled = false;
|
||||
}
|
||||
|
||||
// Custom action.
|
||||
if ( ! $action_handled ) {
|
||||
$screen = get_current_screen()->id;
|
||||
|
||||
/**
|
||||
@@ -1247,13 +1340,11 @@ class ListTable extends WP_List_Table {
|
||||
* @return int Number of orders that were trashed.
|
||||
*/
|
||||
private function do_delete( array $ids, bool $force_delete = false ): int {
|
||||
$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
|
||||
$delete_args = $force_delete ? array( 'force_delete' => true ) : array();
|
||||
$changed = 0;
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
$orders_store->delete( $order, $delete_args );
|
||||
$order->delete( $force_delete );
|
||||
$updated_order = wc_get_order( $id );
|
||||
|
||||
if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
/**
|
||||
* TaxonomiesMetaBox class, renders taxonomy sidebar widget on order edit screen.
|
||||
*/
|
||||
class TaxonomiesMetaBox {
|
||||
|
||||
/**
|
||||
* Order Table data store class.
|
||||
*
|
||||
* @var OrdersTableDataStore
|
||||
*/
|
||||
private $orders_table_data_store;
|
||||
|
||||
/**
|
||||
* Dependency injection init method.
|
||||
*
|
||||
* @param OrdersTableDataStore $orders_table_data_store Order Table data store class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init( OrdersTableDataStore $orders_table_data_store ) {
|
||||
$this->orders_table_data_store = $orders_table_data_store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers meta boxes to be rendered in order edit screen for taxonomies.
|
||||
*
|
||||
* Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it.
|
||||
*
|
||||
* @param string $screen_id Screen ID.
|
||||
* @param string $order_type Order type to register meta boxes for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) {
|
||||
include_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
|
||||
$taxonomies = get_object_taxonomies( $order_type );
|
||||
// All taxonomies.
|
||||
foreach ( $taxonomies as $tax_name ) {
|
||||
$taxonomy = get_taxonomy( $tax_name );
|
||||
if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) {
|
||||
$taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' );
|
||||
}
|
||||
|
||||
if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) {
|
||||
$taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' );
|
||||
}
|
||||
|
||||
$label = $taxonomy->labels->name;
|
||||
|
||||
if ( ! is_taxonomy_hierarchical( $tax_name ) ) {
|
||||
$tax_meta_box_id = 'tagsdiv-' . $tax_name;
|
||||
} else {
|
||||
$tax_meta_box_id = $tax_name . 'div';
|
||||
}
|
||||
|
||||
add_meta_box(
|
||||
$tax_meta_box_id,
|
||||
$label,
|
||||
$taxonomy->meta_box_cb,
|
||||
$screen_id,
|
||||
'side',
|
||||
'core',
|
||||
array(
|
||||
'taxonomy' => $tax_name,
|
||||
'__back_compat_meta_box' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save handler for taxonomy data.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array|null $taxonomy_input Taxonomy input passed from input.
|
||||
*/
|
||||
public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) {
|
||||
if ( ! isset( $taxonomy_input ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input );
|
||||
|
||||
$sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input );
|
||||
$this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy.
|
||||
*
|
||||
* @param array|null $taxonomy_data Nonce verified taxonomy input.
|
||||
*
|
||||
* @return array Sanitized taxonomy input.
|
||||
*/
|
||||
private function sanitize_tax_input( $taxonomy_data ) : array {
|
||||
$sanitized_tax_input = array();
|
||||
if ( ! is_array( $taxonomy_data ) ) {
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
// Convert taxonomy input to term IDs, to avoid ambiguity.
|
||||
foreach ( $taxonomy_data as $taxonomy => $terms ) {
|
||||
$tax_object = get_taxonomy( $taxonomy );
|
||||
if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) {
|
||||
$sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $box Meta box args.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function order_categories_meta_box( $order, $box ) {
|
||||
$post = get_post( $order->get_id() );
|
||||
post_categories_meta_box( $post, $box );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $box Meta box args.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function order_tags_meta_box( $order, $box ) {
|
||||
$post = get_post( $order->get_id() );
|
||||
post_tags_meta_box( $post, $box );
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,48 @@ class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims the lock for the order being edited/created (unless it belongs to someone else).
|
||||
* Also handles the 'claim-lock' action which allows taking over the order forcefully.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_edit_lock() {
|
||||
if ( ! $this->order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$edit_lock = wc_get_container()->get( EditLock::class );
|
||||
|
||||
$locked = $edit_lock->is_locked_by_another_user( $this->order );
|
||||
|
||||
// Take over order?
|
||||
if ( ! empty( $_GET['claim-lock'] ) && wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'claim-lock-' . $this->order->get_id() ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$edit_lock->lock( $this->order );
|
||||
wp_safe_redirect( $this->get_edit_url( $this->order->get_id() ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! $locked ) {
|
||||
$edit_lock->lock( $this->order );
|
||||
}
|
||||
|
||||
add_action(
|
||||
'admin_footer',
|
||||
function() use ( $edit_lock ) {
|
||||
$edit_lock->render_dialog( $this->order );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the page controller, including registering the menu item.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setup(): void {
|
||||
global $plugin_page, $pagenow;
|
||||
|
||||
$this->redirection_controller = new PostsRedirectionController( $this );
|
||||
|
||||
// Register menu.
|
||||
@@ -106,34 +142,81 @@ class PageController {
|
||||
add_action( 'admin_menu', 'register_menu', 9 );
|
||||
}
|
||||
|
||||
// Not on an Orders page.
|
||||
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set_order_type();
|
||||
$this->set_action();
|
||||
|
||||
$page_suffix = ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type );
|
||||
|
||||
self::add_action( 'load-woocommerce_page_wc-orders' . $page_suffix, array( $this, 'handle_load_page_action' ) );
|
||||
self::add_action( 'admin_title', array( $this, 'set_page_title' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initialization for the current action.
|
||||
*/
|
||||
private function handle_load_page_action() {
|
||||
$screen = get_current_screen();
|
||||
$screen->post_type = $this->order_type;
|
||||
|
||||
if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) {
|
||||
$this->{"setup_action_{$this->current_action}"}();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the document title for Orders screens to match what it would be with the shop_order CPT.
|
||||
*
|
||||
* @param string $admin_title The admin screen title before it's filtered.
|
||||
*
|
||||
* @return string The filtered admin title.
|
||||
*/
|
||||
private function set_page_title( $admin_title ) {
|
||||
if ( ! $this->is_order_screen( $this->order_type ) ) {
|
||||
return $admin_title;
|
||||
}
|
||||
|
||||
$wp_order_type = get_post_type_object( $this->order_type );
|
||||
$labels = get_post_type_labels( $wp_order_type );
|
||||
|
||||
if ( $this->is_order_screen( $this->order_type, 'list' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The name of the website.
|
||||
esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->name ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
} elseif ( $this->is_order_screen( $this->order_type, 'edit' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The title of the order 3: The name of the website.
|
||||
esc_html__( '%1$s #%2$s ‹ %3$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->edit_item ),
|
||||
absint( $this->order->get_id() ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
} elseif ( $this->is_order_screen( $this->order_type, 'new' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The name of the website.
|
||||
esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->add_new_item ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
}
|
||||
|
||||
return $admin_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the order type for the current screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function set_order_type() {
|
||||
global $plugin_page, $pagenow;
|
||||
|
||||
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
return;
|
||||
}
|
||||
global $plugin_page;
|
||||
|
||||
$this->order_type = str_replace( array( 'wc-orders--', 'wc-orders' ), '', $plugin_page );
|
||||
$this->order_type = empty( $this->order_type ) ? 'shop_order' : $this->order_type;
|
||||
@@ -207,11 +290,6 @@ class PageController {
|
||||
switch ( $this->current_action ) {
|
||||
case 'edit_order':
|
||||
case 'new_order':
|
||||
if ( ! isset( $this->order_edit_form ) ) {
|
||||
$this->order_edit_form = new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
}
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
$this->order_edit_form->display();
|
||||
break;
|
||||
case 'list_orders':
|
||||
@@ -257,6 +335,22 @@ class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the order edit form for creating or editing an order.
|
||||
*
|
||||
* @see \Automattic\WooCommerce\Internal\Admin\Orders\Edit.
|
||||
* @since 8.1.0
|
||||
*/
|
||||
private function prepare_order_edit_form(): void {
|
||||
if ( ! $this->order || ! in_array( $this->current_action, array( 'new_order', 'edit_order' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->order_edit_form = $this->order_edit_form ?? new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles initialization of the orders edit form.
|
||||
*
|
||||
@@ -266,7 +360,10 @@ class PageController {
|
||||
global $theorder;
|
||||
$this->order = wc_get_order( absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 ) );
|
||||
$this->verify_edit_permission();
|
||||
$this->handle_edit_lock();
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,10 +383,18 @@ class PageController {
|
||||
|
||||
$this->order = new $order_class_name();
|
||||
$this->order->set_object_read( false );
|
||||
$this->order->set_status( 'pending' );
|
||||
$this->order->set_status( 'auto-draft' );
|
||||
$this->order->save();
|
||||
$this->handle_edit_lock();
|
||||
|
||||
// Schedule auto-draft cleanup. We re-use the WP event here on purpose.
|
||||
if ( ! wp_next_scheduled( 'wp_scheduled_auto_draft_delete' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'wp_scheduled_auto_draft_delete' );
|
||||
}
|
||||
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,4 +489,89 @@ class PageController {
|
||||
return admin_url( 'admin.php?page=wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if the current admin screen is related to orders.
|
||||
*
|
||||
* @param string $type Optional. The order type to check for. Default shop_order.
|
||||
* @param string $action Optional. The purpose of the screen to check for. 'list', 'edit', or 'new'.
|
||||
* Leave empty to check for any order screen.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_order_screen( $type = 'shop_order', $action = '' ) : bool {
|
||||
if ( ! did_action( 'current_screen' ) ) {
|
||||
wc_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
// translators: %s is the name of a function.
|
||||
esc_html__( '%s must be called after the current_screen action.', 'woocommerce' ),
|
||||
esc_html( __METHOD__ )
|
||||
),
|
||||
'7.9.0'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid_types = wc_get_order_types( 'view-order' );
|
||||
if ( ! in_array( $type, $valid_types, true ) ) {
|
||||
wc_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
// translators: %s is the name of an order type.
|
||||
esc_html__( '%s is not a valid order type.', 'woocommerce' ),
|
||||
esc_html( $type )
|
||||
),
|
||||
'7.9.0'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
|
||||
if ( $action ) {
|
||||
switch ( $action ) {
|
||||
case 'edit':
|
||||
$is_action = 'edit_order' === $this->current_action;
|
||||
break;
|
||||
case 'list':
|
||||
$is_action = 'list_orders' === $this->current_action;
|
||||
break;
|
||||
case 'new':
|
||||
$is_action = 'new_order' === $this->current_action;
|
||||
break;
|
||||
default:
|
||||
$is_action = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$type_match = $type === $this->order_type;
|
||||
$action_match = ! $action || $is_action;
|
||||
} else {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( $action ) {
|
||||
switch ( $action ) {
|
||||
case 'edit':
|
||||
$screen_match = 'post' === $screen->base && filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
|
||||
break;
|
||||
case 'list':
|
||||
$screen_match = 'edit' === $screen->base;
|
||||
break;
|
||||
case 'new':
|
||||
$screen_match = 'post' === $screen->base && 'add' === $screen->action;
|
||||
break;
|
||||
default:
|
||||
$screen_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$type_match = $type === $screen->post_type;
|
||||
$action_match = ! $action || $screen_match;
|
||||
}
|
||||
|
||||
return $type_match && $action_match;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class PostsRedirectionController {
|
||||
$new_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'order' => $posts,
|
||||
'id' => $posts,
|
||||
'_wp_http_referer' => $this->page_controller->get_orders_url(),
|
||||
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ use Automattic\WooCommerce\Admin\API\Plugins;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use WC_Marketplace_Suggestions;
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,7 @@ class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks extra neccessary data into the component settings array already set in WooCommerce core.
|
||||
* Hooks extra necessary data into the component settings array already set in WooCommerce core.
|
||||
*
|
||||
* @param array $settings Array of component settings.
|
||||
* @return array Array of component settings.
|
||||
@@ -233,9 +235,32 @@ class Settings {
|
||||
$settings['connectNonce'] = wp_create_nonce( 'connect' );
|
||||
$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );
|
||||
|
||||
$settings['features'] = $this->get_features();
|
||||
|
||||
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes non necesary feature properties for the client side.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_features() {
|
||||
$features = FeaturesUtil::get_features( true, true );
|
||||
$new_features = array();
|
||||
|
||||
foreach ( array_keys( $features ) as $feature_id ) {
|
||||
$new_features[ $feature_id ] = array(
|
||||
'is_enabled' => $features[ $feature_id ]['is_enabled'],
|
||||
'is_experimental' => $features[ $feature_id ]['is_experimental'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
return $new_features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the admin settings for use in the WC REST API
|
||||
*
|
||||
|
||||
@@ -22,6 +22,14 @@ class WCAdminAssets {
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* An array of dependencies that have been preloaded (to avoid duplicates).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $preloaded_dependencies;
|
||||
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
@@ -238,7 +246,7 @@ class WCAdminAssets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all the neccessary scripts and styles to show the admin experience.
|
||||
* Registers all the necessary scripts and styles to show the admin experience.
|
||||
*/
|
||||
public function register_scripts() {
|
||||
if ( ! function_exists( 'wp_set_script_translations' ) ) {
|
||||
@@ -278,6 +286,7 @@ class WCAdminAssets {
|
||||
'wc-date',
|
||||
'wc-components',
|
||||
'wc-customer-effort-score',
|
||||
'wc-experimental',
|
||||
WC_ADMIN_APP,
|
||||
);
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ class WCAdminUser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to retrive user data fields.
|
||||
* Helper to retrieve user data fields.
|
||||
*
|
||||
* Migrates old key prefixes as well.
|
||||
*
|
||||
|
||||
@@ -141,6 +141,15 @@ class Init {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merchant WooPay eligibility.
|
||||
*/
|
||||
public static function is_woopay_eligible() {
|
||||
$wcpay_promotion = self::get_wc_pay_promotion_spec();
|
||||
|
||||
return $wcpay_promotion && 'woocommerce_payments:woopay' === $wcpay_promotion->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specs transient.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;
|
||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Class WCPayWelcomePage
|
||||
@@ -11,121 +12,106 @@ use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
* @package Automattic\WooCommerce\Admin\Features
|
||||
*/
|
||||
class WcPayWelcomePage {
|
||||
const CACHE_TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
|
||||
const HAD_WCPAY_OPTION_NAME = 'wcpay_was_in_use';
|
||||
|
||||
const EXPERIMENT_NAME = 'woocommerce_payments_menu_promo_us_2022';
|
||||
const OTHER_GATEWAYS = [
|
||||
'affirm',
|
||||
'afterpay',
|
||||
'amazon_payments_advanced_express',
|
||||
'amazon_payments_advanced',
|
||||
'authorize_net_cim_credit_card',
|
||||
'authorize_net_cim_echeck',
|
||||
'bacs',
|
||||
'bambora_credit_card',
|
||||
'braintree_credit_card',
|
||||
'braintree_paypal',
|
||||
'chase_paymentech',
|
||||
'cybersource_credit_card',
|
||||
'elavon_converge_credit_card',
|
||||
'elavon_converge_echeck',
|
||||
'gocardless',
|
||||
'intuit_payments_credit_card',
|
||||
'intuit_payments_echeck',
|
||||
'kco',
|
||||
'klarna_payments',
|
||||
'payfast',
|
||||
'paypal',
|
||||
'paytrace',
|
||||
'ppcp-gateway',
|
||||
'psigate',
|
||||
'sagepaymentsusaapi',
|
||||
'square_credit_card',
|
||||
'stripe_alipay',
|
||||
'stripe_multibanco',
|
||||
'stripe',
|
||||
'trustcommerce',
|
||||
'usa_epay_credit_card',
|
||||
];
|
||||
/**
|
||||
* Plugin instance.
|
||||
*
|
||||
* @var WcPayWelcomePage
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Main Instance.
|
||||
*/
|
||||
public static function instance() {
|
||||
self::$instance = is_null( self::$instance ) ? new self() : self::$instance;
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eligible incentive for the store.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $incentive = null;
|
||||
|
||||
/**
|
||||
* WCPayWelcomePage constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', array( $this, 'register_payments_welcome_page' ) );
|
||||
add_action( 'admin_menu', [ $this, 'register_payments_welcome_page' ] );
|
||||
add_filter( 'woocommerce_admin_shared_settings', [ $this, 'shared_settings' ] );
|
||||
add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the WooCommerce Payments welcome page.
|
||||
* Whether the WooPayments welcome page should be visible.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function register_payments_welcome_page() {
|
||||
global $menu;
|
||||
|
||||
// WC Payment must not be installed.
|
||||
if ( WooCommercePayments::is_installed() ) {
|
||||
return;
|
||||
public function must_be_visible(): bool {
|
||||
// The WooPayments plugin must not be active.
|
||||
if ( $this->is_wcpay_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Live store for at least 90 days.
|
||||
if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS * 90 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be a US based business.
|
||||
if ( WC()->countries->get_base_country() !== 'US' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Has another payment gateway installed.
|
||||
if ( ! $this->is_another_payment_gateway_installed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No existing WCPay account.
|
||||
if ( $this->has_wcpay_account() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Suggestions may be disabled via a setting.
|
||||
// Suggestions not disabled via a setting.
|
||||
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter allow marketplace suggestions.
|
||||
*
|
||||
* User can disabled all suggestions via filter.
|
||||
* User can disable all suggestions via filter.
|
||||
*
|
||||
* @since 3.6.0
|
||||
*/
|
||||
if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An incentive must be available.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Incentive not manually dismissed.
|
||||
if ( $this->is_incentive_dismissed() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the WooPayments welcome page.
|
||||
*/
|
||||
public function register_payments_welcome_page() {
|
||||
global $menu;
|
||||
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually dismissed.
|
||||
if ( get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) === 'yes' ) {
|
||||
return;
|
||||
}
|
||||
$menu_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NTIiIGhlaWdodD0iNjg0Ij48cGF0aCBmaWxsPSIjYTJhYWIyIiBkPSJNODIgODZ2NTEyaDY4NFY4NlptMCA1OThjLTQ4IDAtODQtMzgtODQtODZWODZDLTIgMzggMzQgMCA4MiAwaDY4NGM0OCAwIDg0IDM4IDg0IDg2djUxMmMwIDQ4LTM2IDg2LTg0IDg2em0zODQtNTU2djQ0aDg2djg0SDM4MnY0NGgxMjhjMjQgMCA0MiAxOCA0MiA0MnYxMjhjMCAyNC0xOCA0Mi00MiA0MmgtNDR2NDRoLTg0di00NGgtODZ2LTg0aDE3MHYtNDRIMzM4Yy0yNCAwLTQyLTE4LTQyLTQyVjIxNGMwLTI0IDE4LTQyIDQyLTQyaDQ0di00NHoiLz48L3N2Zz4=';
|
||||
|
||||
// Users must be in the experiment.
|
||||
if ( ! $this->is_user_in_treatment_mode() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo=';
|
||||
|
||||
$menu_data = array(
|
||||
$menu_data = [
|
||||
'id' => 'wc-calypso-bridge-payments-welcome-page',
|
||||
'title' => __( 'Payments', 'woocommerce' ),
|
||||
'title' => esc_html__( 'Payments', 'woocommerce' ),
|
||||
'path' => '/wc-pay-welcome-page',
|
||||
'position' => '56',
|
||||
'nav_args' => [
|
||||
'title' => __( 'WooCommerce Payments', 'woocommerce' ),
|
||||
'title' => esc_html__( 'WooPayments', 'woocommerce' ),
|
||||
'is_category' => false,
|
||||
'menuId' => 'plugins',
|
||||
'is_top_level' => true,
|
||||
],
|
||||
'icon' => $menu_icon,
|
||||
);
|
||||
];
|
||||
|
||||
wc_admin_register_page( $menu_data );
|
||||
|
||||
@@ -134,71 +120,332 @@ class WcPayWelcomePage {
|
||||
// We need to register this menu via add_menu_page so that it doesn't become a child of
|
||||
// WooCommerce menu.
|
||||
if ( get_option( 'woocommerce_navigation_enabled', 'no' ) === 'yes' ) {
|
||||
$menu_with_nav_data = array(
|
||||
__( 'Payments', 'woocommerce' ),
|
||||
__( 'Payments', 'woocommerce' ),
|
||||
$menu_with_nav_data = [
|
||||
esc_html__( 'Payments', 'woocommerce' ),
|
||||
esc_html__( 'Payments', 'woocommerce' ),
|
||||
'view_woocommerce_reports',
|
||||
'admin.php?page=wc-admin&path=/wc-pay-welcome-page',
|
||||
null,
|
||||
$menu_icon,
|
||||
56,
|
||||
);
|
||||
];
|
||||
|
||||
call_user_func_array( 'add_menu_page', $menu_with_nav_data );
|
||||
}
|
||||
|
||||
// Add badge.
|
||||
$badge = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
foreach ( $menu as $index => $menu_item ) {
|
||||
if ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] ) {
|
||||
//phpcs:ignore
|
||||
$menu[ $index ][0] .= ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
// Only add the badge markup if not already present and the menu item is the WooPayments menu item.
|
||||
if ( false === strpos( $menu_item[0], $badge )
|
||||
&& ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] )
|
||||
) {
|
||||
$menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
// One menu item with a badge is more than enough.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a WCPay account exists. By checking account data cache.
|
||||
* Adds shared settings for the WooPayments incentive.
|
||||
*
|
||||
* @param array $settings Shared settings.
|
||||
* @return array
|
||||
*/
|
||||
public function shared_settings( $settings ): array {
|
||||
// Return early if not on a wc-admin powered page.
|
||||
if ( ! PageController::is_admin_page() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$settings['wcpayWelcomePageIncentive'] = $this->get_incentive();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds allowed promo notes from the WooPayments incentive.
|
||||
*
|
||||
* @param array $promo_notes Allowed promo notes.
|
||||
* @return array
|
||||
*/
|
||||
public function allowed_promo_notes( $promo_notes = [] ): array {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
// Add our incentive ID to the promo notes.
|
||||
$promo_notes[] = $this->get_incentive()['id'];
|
||||
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WooPayments payment gateway is active and set up or was at some point,
|
||||
* or there are orders processed with it, at some moment.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_wcpay_account(): bool {
|
||||
$account_data = get_option( 'wcpay_account_data' );
|
||||
return isset( $account_data['data'] ) && is_array( $account_data['data'] ) && ! empty( $account_data['data'] );
|
||||
private function has_wcpay(): bool {
|
||||
// First, get the stored value, if it exists.
|
||||
// This way we avoid costly DB queries and API calls.
|
||||
// Basically, we only want to know if WooPayments was in use in the past.
|
||||
// Since the past can't be changed, neither can this value.
|
||||
$had_wcpay = get_option( self::HAD_WCPAY_OPTION_NAME );
|
||||
if ( false !== $had_wcpay ) {
|
||||
return $had_wcpay === 'yes';
|
||||
}
|
||||
|
||||
// We need to determine the value.
|
||||
// Start with the assumption that the store didn't have WooPayments in use.
|
||||
$had_wcpay = false;
|
||||
|
||||
// We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache.
|
||||
// This implies that WooPayments was active at some point and that it was connected.
|
||||
// If WooPayments is active right now, we will not get to this point since the plugin is active check is done first.
|
||||
if ( $this->has_wcpay_account_data() ) {
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
// If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
|
||||
if ( false === $had_wcpay && ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'payment_method' => 'woocommerce_payments',
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
) ) {
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
// Store the value for future use.
|
||||
update_option( self::HAD_WCPAY_OPTION_NAME, $had_wcpay ? 'yes' : 'no' );
|
||||
|
||||
return $had_wcpay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user is in the experiment.
|
||||
* Check if the WooPayments plugin is active.
|
||||
*
|
||||
* @return bool Whether the user is in the treatment group.
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_user_in_treatment_mode() {
|
||||
$anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
|
||||
$allow_tracking = get_option( 'woocommerce_allow_tracking' ) === 'yes';
|
||||
$abtest = new \WooCommerce\Admin\Experimental_Abtest(
|
||||
$anon_id,
|
||||
'woocommerce',
|
||||
$allow_tracking
|
||||
);
|
||||
|
||||
return $abtest->get_variation( self::EXPERIMENT_NAME ) === 'treatment';
|
||||
private function is_wcpay_active(): bool {
|
||||
return class_exists( '\WC_Payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is another payment gateway installed using a static list of US gateways from WC Store.
|
||||
* Check if there is meaningful data in the WooPayments account cache.
|
||||
*
|
||||
* @return bool Whether there is another payment gateway installed.
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_another_payment_gateway_installed() {
|
||||
$available_gateways = wp_list_pluck( WC()->payment_gateways()->get_available_payment_gateways(), 'id' );
|
||||
|
||||
foreach ( $available_gateways as $gateway ) {
|
||||
if ( in_array( $gateway, self::OTHER_GATEWAYS, true ) ) {
|
||||
return true;
|
||||
}
|
||||
private function has_wcpay_account_data(): bool {
|
||||
$account_data = get_option( 'wcpay_account_data', [] );
|
||||
if ( ! empty( $account_data['data']['account_id'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current incentive has been manually dismissed.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_incentive_dismissed(): bool {
|
||||
$dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] );
|
||||
|
||||
// If there are no dismissed incentives, return early.
|
||||
if ( empty( $dismissed_incentives ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return early if there is no eligible incentive.
|
||||
$incentive = $this->get_incentive();
|
||||
if ( empty( $incentive ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search the incentive ID in the dismissed incentives list.
|
||||
if ( in_array( $incentive['id'], $dismissed_incentives, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and caches eligible incentive from the WooPayments API.
|
||||
*
|
||||
* @return array|null Array of eligible incentive or null.
|
||||
*/
|
||||
private function get_incentive(): ?array {
|
||||
// Return in-memory cached incentive if it is set.
|
||||
if ( isset( $this->incentive ) ) {
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Get the cached data.
|
||||
$cache = get_transient( self::CACHE_TRANSIENT_NAME );
|
||||
|
||||
// If the cached data is not expired and it's a WP_Error,
|
||||
// it means there was an API error previously and we should not retry just yet.
|
||||
if ( is_wp_error( $cache ) ) {
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Gather the store context data.
|
||||
$store_context = [
|
||||
// Store ISO-2 country code, e.g. `US`.
|
||||
'country' => WC()->countries->get_base_country(),
|
||||
// Store locale, e.g. `en_US`.
|
||||
'locale' => get_locale(),
|
||||
// WooCommerce active for duration in seconds.
|
||||
'active_for' => WCAdminHelper::get_wcadmin_active_for_in_seconds(),
|
||||
// Whether the store has paid orders in the last 90 days.
|
||||
'has_orders' => ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'status' => [ 'wc-completed', 'wc-processing' ],
|
||||
'date_created' => '>=' . strtotime( '-90 days' ),
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
),
|
||||
// Whether the store has at least one payment gateway enabled.
|
||||
'has_payments' => ! empty( WC()->payment_gateways()->get_available_payment_gateways() ),
|
||||
'has_wcpay' => $this->has_wcpay(),
|
||||
];
|
||||
|
||||
// Fingerprint the store context through a hash of certain entries.
|
||||
$store_context_hash = $this->generate_context_hash( $store_context );
|
||||
|
||||
// Use the transient cached incentive if it exists, it is not expired,
|
||||
// and the store context hasn't changed since we last requested from the WooPayments API (based on context hash).
|
||||
if ( false !== $cache
|
||||
&& ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] )
|
||||
&& hash_equals( $store_context_hash, $cache['context_hash'] ) ) {
|
||||
|
||||
// We have a store context hash and it matches with the current context one.
|
||||
// We can use the cached incentive data.
|
||||
// Store the incentive in the in-memory cache and return it.
|
||||
$this->incentive = $cache['incentive'] ?? [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// By this point, we have an expired transient or the store context has changed.
|
||||
// Query for incentives by calling the WooPayments API.
|
||||
$url = add_query_arg(
|
||||
$store_context,
|
||||
'https://public-api.wordpress.com/wpcom/v2/wcpay/incentives',
|
||||
);
|
||||
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
[
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
]
|
||||
);
|
||||
|
||||
// Return early if there is an error, waiting 6 hours before the next attempt.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
// Store a trimmed down, lightweight error.
|
||||
$error = new \WP_Error(
|
||||
$response->get_error_code(),
|
||||
$response->get_error_message(),
|
||||
wp_remote_retrieve_response_code( $response )
|
||||
);
|
||||
// Store the error in the transient so we know this is due to an API error.
|
||||
set_transient( self::CACHE_TRANSIENT_NAME, $error, HOUR_IN_SECONDS * 6 );
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
$cache_for = wp_remote_retrieve_header( $response, 'cache-for' );
|
||||
// Initialize the in-memory cache.
|
||||
$this->incentive = [];
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
// Decode the results, falling back to an empty array.
|
||||
$results = json_decode( wp_remote_retrieve_body( $response ), true ) ?? [];
|
||||
|
||||
// Find all `welcome_page` incentives.
|
||||
$incentives = array_filter(
|
||||
$results,
|
||||
function( $incentive ) {
|
||||
return 'welcome_page' === $incentive['type'];
|
||||
}
|
||||
);
|
||||
|
||||
// Use the first found matching incentive or empty array if none was found.
|
||||
// Store incentive in the in-memory cache.
|
||||
$this->incentive = empty( $incentives ) ? [] : reset( $incentives );
|
||||
}
|
||||
|
||||
// Skip transient cache if `cache-for` header equals zero.
|
||||
if ( '0' === $cache_for ) {
|
||||
// If we have a transient cache that is not expired, delete it so there are no leftovers.
|
||||
if ( false !== $cache ) {
|
||||
delete_transient( self::CACHE_TRANSIENT_NAME );
|
||||
}
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Store incentive in transient cache (together with the context hash) for the given number of seconds
|
||||
// or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched.
|
||||
set_transient(
|
||||
self::CACHE_TRANSIENT_NAME,
|
||||
[
|
||||
'incentive' => $this->incentive,
|
||||
'context_hash' => $store_context_hash,
|
||||
'timestamp' => time(),
|
||||
],
|
||||
! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS
|
||||
);
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash from the store context data.
|
||||
*
|
||||
* @param array $context The store context data.
|
||||
*
|
||||
* @return string The context hash.
|
||||
*/
|
||||
private function generate_context_hash( array $context ): string {
|
||||
// Include only certain entries in the context hash.
|
||||
// We need only discrete, user-interaction dependent data.
|
||||
// Entries like `active_for` have no place in the hash generation since they change automatically.
|
||||
return md5(
|
||||
wp_json_encode(
|
||||
[
|
||||
'country' => $context['country'] ?? '',
|
||||
'locale' => $context['locale'] ?? '',
|
||||
'has_orders' => $context['has_orders'] ?? false,
|
||||
'has_payments' => $context['has_payments'] ?? false,
|
||||
'has_wcpay' => $context['has_wcpay'] ?? false,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,18 +138,17 @@ class BatchProcessingController {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch_processor = $this->get_processor_instance( $processor_class_name );
|
||||
$pending_count_before = $batch_processor->get_total_pending_count();
|
||||
$error = $this->process_next_batch_for_single_processor_core( $batch_processor );
|
||||
$pending_count_after = $batch_processor->get_total_pending_count();
|
||||
if ( ( $error instanceof \Exception ) && $pending_count_before === $pending_count_after ) {
|
||||
$batch_processor = $this->get_processor_instance( $processor_class_name );
|
||||
$error = $this->process_next_batch_for_single_processor_core( $batch_processor );
|
||||
$still_pending = count( $batch_processor->get_next_batch_to_process( 1 ) ) > 0;
|
||||
if ( ( $error instanceof \Exception ) ) {
|
||||
// The batch processing failed and no items were processed:
|
||||
// reschedule the processing with a delay, and also throw the error
|
||||
// so Action Scheduler will ignore the rescheduling if this happens repeatedly.
|
||||
$this->schedule_batch_processing( $processor_class_name, true );
|
||||
throw $error;
|
||||
}
|
||||
if ( $pending_count_after > 0 ) {
|
||||
if ( $still_pending ) {
|
||||
$this->schedule_batch_processing( $processor_class_name );
|
||||
} else {
|
||||
$this->dequeue_processor( $processor_class_name );
|
||||
@@ -369,13 +368,12 @@ class BatchProcessingController {
|
||||
$batch_detail_string = '';
|
||||
// Log only first and last, as the entire batch may be too big.
|
||||
if ( count( $batch ) > 0 ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging is for debugging.
|
||||
$batch_detail_string = '\n' . print_r(
|
||||
$batch_detail_string = "\n" . wp_json_encode(
|
||||
array(
|
||||
'batch_start' => $batch[0],
|
||||
'batch_end' => end( $batch ),
|
||||
),
|
||||
true
|
||||
JSON_PRETTY_PRINT
|
||||
);
|
||||
}
|
||||
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}" . $batch_detail_string;
|
||||
|
||||
@@ -194,4 +194,41 @@ abstract class CustomMetaDataStore {
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata by meta key.
|
||||
*
|
||||
* @param \WC_Abstract_Order $object Object ID.
|
||||
* @param string $meta_key Meta key.
|
||||
*
|
||||
* @return \stdClass|bool Metadata object or FALSE if not found.
|
||||
*/
|
||||
public function get_metadata_by_key( &$object, string $meta_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$db_info = $this->get_db_info();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$meta = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT {$db_info['meta_id_field']}, meta_key, meta_value, {$db_info['object_id_field']} FROM {$db_info['table']} WHERE meta_key = %s AND {$db_info['object_id_field']} = %d",
|
||||
$meta_key,
|
||||
$object->get_id(),
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
if ( empty( $meta ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $meta as $row ) {
|
||||
if ( isset( $row->meta_value ) ) {
|
||||
$row->meta_value = maybe_unserialize( $row->meta_value );
|
||||
}
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Caches\OrderCache;
|
||||
use Automattic\WooCommerce\Caches\OrderCacheController;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -32,11 +33,6 @@ class CustomOrdersTableController {
|
||||
*/
|
||||
public const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled';
|
||||
|
||||
/**
|
||||
* The name of the option that tells that the authoritative table must be flipped once sync finishes.
|
||||
*/
|
||||
private const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';
|
||||
|
||||
/**
|
||||
* The name of the option that tells whether database transactions are to be used or not for data synchronization.
|
||||
*/
|
||||
@@ -47,7 +43,7 @@ class CustomOrdersTableController {
|
||||
*/
|
||||
public const DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION = 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync';
|
||||
|
||||
public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'REPEATABLE READ';
|
||||
public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'READ UNCOMMITTED';
|
||||
|
||||
/**
|
||||
* The data store object to use.
|
||||
@@ -98,6 +94,13 @@ class CustomOrdersTableController {
|
||||
*/
|
||||
private $order_cache_controller;
|
||||
|
||||
/**
|
||||
* The plugin util object to use.
|
||||
*
|
||||
* @var PluginUtil
|
||||
*/
|
||||
private $plugin_util;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
@@ -112,14 +115,12 @@ class CustomOrdersTableController {
|
||||
self::add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'get_settings_sections' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'get_settings' ), 999, 2 );
|
||||
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
|
||||
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
|
||||
self::add_filter( DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, array( $this, 'process_sync_finished' ), 10, 0 );
|
||||
self::add_action( 'woocommerce_update_options_advanced_custom_data_stores', array( $this, 'process_options_updated' ), 10, 0 );
|
||||
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_data_sync_option_changed' ), 10, 1 );
|
||||
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
|
||||
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enabled_changed' ), 10, 2 );
|
||||
self::add_action( 'woocommerce_feature_setting', array( $this, 'get_hpos_feature_setting' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +134,7 @@ class CustomOrdersTableController {
|
||||
* @param FeaturesController $features_controller The features controller instance to use.
|
||||
* @param OrderCache $order_cache The order cache engine to use.
|
||||
* @param OrderCacheController $order_cache_controller The order cache controller to use.
|
||||
* @param PluginUtil $plugin_util The plugin util to use.
|
||||
*/
|
||||
final public function init(
|
||||
OrdersTableDataStore $data_store,
|
||||
@@ -141,7 +143,9 @@ class CustomOrdersTableController {
|
||||
BatchProcessingController $batch_processing_controller,
|
||||
FeaturesController $features_controller,
|
||||
OrderCache $order_cache,
|
||||
OrderCacheController $order_cache_controller ) {
|
||||
OrderCacheController $order_cache_controller,
|
||||
PluginUtil $plugin_util
|
||||
) {
|
||||
$this->data_store = $data_store;
|
||||
$this->data_synchronizer = $data_synchronizer;
|
||||
$this->batch_processing_controller = $batch_processing_controller;
|
||||
@@ -149,6 +153,7 @@ class CustomOrdersTableController {
|
||||
$this->features_controller = $features_controller;
|
||||
$this->order_cache = $order_cache;
|
||||
$this->order_cache_controller = $order_cache_controller;
|
||||
$this->plugin_util = $plugin_util;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +162,7 @@ class CustomOrdersTableController {
|
||||
* @return bool True if the feature is visible.
|
||||
*/
|
||||
public function is_feature_visible(): bool {
|
||||
return $this->features_controller->feature_is_enabled( 'custom_order_tables' );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,24 +183,6 @@ class CustomOrdersTableController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the feature, so that no entries will be added to the debug tools page.
|
||||
*
|
||||
* This method shouldn't be used anymore, see the FeaturesController class.
|
||||
*/
|
||||
public function hide_feature() {
|
||||
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
|
||||
wc_doing_it_wrong(
|
||||
$class_and_method,
|
||||
sprintf(
|
||||
// translators: %1$s the name of the class and method used.
|
||||
__( '%1$s: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.', 'woocommerce' ),
|
||||
$class_and_method
|
||||
),
|
||||
'7.0'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the custom orders table usage enabled via settings?
|
||||
* This can be true only if the feature is enabled and a table regeneration has been completed.
|
||||
@@ -261,9 +248,9 @@ class CustomOrdersTableController {
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
if ( $this->is_feature_visible() ) {
|
||||
if ( $this->custom_orders_table_usage_is_enabled() || $this->data_synchronizer->data_sync_is_enabled() ) {
|
||||
$disabled = true;
|
||||
$message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" feature is disabled (via Settings > Advanced > Features).', 'woocommerce' );
|
||||
$message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" is not authoritative and sync is disabled (via Settings > Advanced > Features).', 'woocommerce' );
|
||||
} else {
|
||||
$disabled = false;
|
||||
$message = __( 'This will delete the custom orders tables. To create them again enable the "High-Performance order storage" feature (via Settings > Advanced > Features).', 'woocommerce' );
|
||||
@@ -278,7 +265,7 @@ class CustomOrdersTableController {
|
||||
),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function () {
|
||||
$this->features_controller->change_feature_enable( 'custom_order_tables', false );
|
||||
$this->features_controller->change_feature_enable( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, false );
|
||||
$this->delete_custom_orders_tables();
|
||||
return __( 'Custom orders tables have been deleted.', 'woocommerce' );
|
||||
},
|
||||
@@ -313,154 +300,13 @@ class CustomOrdersTableController {
|
||||
*/
|
||||
private function delete_custom_orders_tables() {
|
||||
if ( $this->custom_orders_table_usage_is_enabled() ) {
|
||||
throw new \Exception( "Can't delete the custom orders tables: they are currently in use (via Settings > Advanced > Custom data stores)." );
|
||||
throw new \Exception( "Can't delete the custom orders tables: they are currently in use (via Settings > Advanced > Features)." );
|
||||
}
|
||||
|
||||
delete_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
|
||||
$this->data_synchronizer->delete_database_tables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings sections for the "Advanced" tab, with a "Custom data stores" section added if appropriate.
|
||||
*
|
||||
* @param array $sections The original settings sections array.
|
||||
* @return array The updated settings sections array.
|
||||
*/
|
||||
private function get_settings_sections( array $sections ): array {
|
||||
if ( ! $this->is_feature_visible() ) {
|
||||
return $sections;
|
||||
}
|
||||
|
||||
$sections['custom_data_stores'] = __( 'Custom data stores', 'woocommerce' );
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings for the "Custom data stores" section in the "Advanced" tab,
|
||||
* with entries for managing the custom orders tables if appropriate.
|
||||
*
|
||||
* @param array $settings The original settings array.
|
||||
* @param string $section_id The settings section to get the settings for.
|
||||
* @return array The updated settings array.
|
||||
*/
|
||||
private function get_settings( array $settings, string $section_id ): array {
|
||||
if ( ! $this->is_feature_visible() || 'custom_data_stores' !== $section_id ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$settings[] = array(
|
||||
'title' => __( 'Custom orders tables', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'id' => 'cot-title',
|
||||
'desc' => sprintf(
|
||||
/* translators: %1$s = <strong> tag, %2$s = </strong> tag. */
|
||||
__( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ),
|
||||
'<strong>',
|
||||
'</strong>'
|
||||
),
|
||||
);
|
||||
|
||||
$sync_status = $this->data_synchronizer->get_sync_status();
|
||||
$sync_is_pending = 0 !== $sync_status['current_pending_count'];
|
||||
|
||||
$settings[] = array(
|
||||
'title' => __( 'Data store for orders', 'woocommerce' ),
|
||||
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
'default' => 'no',
|
||||
'type' => 'radio',
|
||||
'options' => array(
|
||||
'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ),
|
||||
'no' => __( 'Use the WordPress posts table', 'woocommerce' ),
|
||||
),
|
||||
'checkboxgroup' => 'start',
|
||||
'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(),
|
||||
);
|
||||
|
||||
if ( $sync_is_pending ) {
|
||||
$initial_pending_count = $sync_status['initial_pending_count'];
|
||||
$current_pending_count = $sync_status['current_pending_count'];
|
||||
if ( $initial_pending_count ) {
|
||||
$text =
|
||||
sprintf(
|
||||
/* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */
|
||||
_n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ),
|
||||
$current_pending_count,
|
||||
$initial_pending_count
|
||||
);
|
||||
} else {
|
||||
$text =
|
||||
/* translators: %s=initial number of orders pending sync */
|
||||
sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' );
|
||||
}
|
||||
|
||||
if ( $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) {
|
||||
$text .= __( "<br/>Synchronization for these orders is currently in progress.<br/>The authoritative table can't be changed until sync completes.", 'woocommerce' );
|
||||
} else {
|
||||
$text .= __( "<br/>The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' );
|
||||
}
|
||||
|
||||
$settings[] = array(
|
||||
'type' => 'info',
|
||||
'id' => 'cot-out-of-sync-warning',
|
||||
'css' => 'color: #C00000',
|
||||
'text' => $text,
|
||||
);
|
||||
}
|
||||
|
||||
$settings[] = array(
|
||||
'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ),
|
||||
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
if ( $sync_is_pending ) {
|
||||
if ( $this->data_synchronizer->data_sync_is_enabled() ) {
|
||||
$message = $this->custom_orders_table_usage_is_enabled() ?
|
||||
__( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) :
|
||||
__( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' );
|
||||
$settings[] = array(
|
||||
'desc' => $message,
|
||||
'id' => self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$settings[] = array(
|
||||
'desc' => __( 'Use database transactions for the orders data synchronization', 'woocommerce' ),
|
||||
'id' => self::USE_DB_TRANSACTIONS_OPTION,
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$isolation_level_names = self::get_valid_transaction_isolation_levels();
|
||||
$settings[] = array(
|
||||
'desc' => __( 'Database transaction isolation level to use', 'woocommerce' ),
|
||||
'id' => self::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION,
|
||||
'type' => 'select',
|
||||
'options' => array_combine( $isolation_level_names, $isolation_level_names ),
|
||||
'default' => self::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL,
|
||||
);
|
||||
|
||||
$settings[] = array( 'type' => 'sectionend' );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the valid database transaction isolation level names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_valid_transaction_isolation_levels() {
|
||||
return array(
|
||||
'REPEATABLE READ',
|
||||
'READ COMMITTED',
|
||||
'READ UNCOMMITTED',
|
||||
'SERIALIZABLE',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the individual setting updated hook.
|
||||
*
|
||||
@@ -508,41 +354,18 @@ class CustomOrdersTableController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the synchronization finished hook.
|
||||
* Here we switch the authoritative table if needed.
|
||||
* Handler for the all settings updated hook.
|
||||
*
|
||||
* @param string $feature_id Feature ID.
|
||||
*/
|
||||
private function process_sync_finished() {
|
||||
if ( ! $this->auto_flip_authoritative_table_enabled() ) {
|
||||
private function handle_data_sync_option_changed( string $feature_id ) {
|
||||
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION !== $feature_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
|
||||
|
||||
if ( $this->custom_orders_table_usage_is_enabled() ) {
|
||||
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
|
||||
} else {
|
||||
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the automatic authoritative table switch setting set?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function auto_flip_authoritative_table_enabled(): bool {
|
||||
return get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ) === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the all settings updated hook.
|
||||
*/
|
||||
private function process_options_updated() {
|
||||
$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
|
||||
|
||||
// Disabling the sync implies disabling the automatic authoritative table switch too.
|
||||
if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) {
|
||||
update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
$this->data_synchronizer->create_database_tables();
|
||||
}
|
||||
|
||||
// Enabling/disabling the sync implies starting/stopping it too, if needed.
|
||||
@@ -565,12 +388,11 @@ class CustomOrdersTableController {
|
||||
* @param bool $is_enabled True if the feature is being enabled, false if it's being disabled.
|
||||
*/
|
||||
private function handle_feature_enabled_changed( $feature_id, $is_enabled ): void {
|
||||
if ( 'custom_order_tables' !== $feature_id || ! $is_enabled ) {
|
||||
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $feature_id || ! $is_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
update_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'no' );
|
||||
$this->create_custom_orders_tables( false );
|
||||
}
|
||||
}
|
||||
@@ -606,5 +428,106 @@ class CustomOrdersTableController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HPOS setting for rendering in Features section of the settings page.
|
||||
*
|
||||
* @param array $feature_setting HPOS feature value as defined in the feature controller.
|
||||
* @param string $feature_id ID of the feature.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
*/
|
||||
private function get_hpos_feature_setting( array $feature_setting, string $feature_id ) {
|
||||
if ( ! in_array( $feature_id, array( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'custom_order_tables' ), true ) ) {
|
||||
return $feature_setting;
|
||||
}
|
||||
|
||||
if ( 'yes' === get_transient( 'wc_installing' ) ) {
|
||||
return $feature_setting;
|
||||
}
|
||||
|
||||
$sync_status = $this->data_synchronizer->get_sync_status();
|
||||
switch ( $feature_id ) {
|
||||
case self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
|
||||
return $this->get_hpos_setting_for_feature( $sync_status );
|
||||
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
|
||||
return $this->get_hpos_setting_for_sync( $sync_status );
|
||||
case 'custom_order_tables':
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page.
|
||||
*
|
||||
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
*/
|
||||
private function get_hpos_setting_for_feature( $sync_status ) {
|
||||
$hpos_enabled = $this->custom_orders_table_usage_is_enabled();
|
||||
$plugin_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
$plugin_incompat_warning = $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_info );
|
||||
$sync_complete = 0 === $sync_status['current_pending_count'];
|
||||
$disabled_option = array();
|
||||
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
|
||||
$disabled_option = array( 'yes' );
|
||||
}
|
||||
if ( ! $sync_complete ) {
|
||||
$disabled_option = array( 'yes', 'no' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
'title' => __( 'Data storage for orders', 'woocommerce' ),
|
||||
'type' => 'radio',
|
||||
'options' => array(
|
||||
'no' => __( 'WordPress post tables', 'woocommerce' ),
|
||||
'yes' => __( 'High performance order storage (new)', 'woocommerce' ),
|
||||
),
|
||||
'value' => $hpos_enabled ? 'yes' : 'no',
|
||||
'disabled' => $disabled_option,
|
||||
'desc' => $plugin_incompat_warning,
|
||||
'desc_at_end' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the setting for rendering sync enabling setting block in Features section of the settings page.
|
||||
*
|
||||
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
*/
|
||||
private function get_hpos_setting_for_sync( $sync_status ) {
|
||||
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
|
||||
$sync_enabled = get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
|
||||
$sync_message = '';
|
||||
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_message = sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
__( 'Currently syncing orders... %d pending', 'woocommerce' ),
|
||||
$sync_status['current_pending_count']
|
||||
);
|
||||
} elseif ( $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_message = sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
_n(
|
||||
'Sync %d pending order. You can switch data storage for orders only when posts and orders table are in sync.',
|
||||
'Sync %d pending orders. You can switch data storage for orders only when posts and orders table are in sync.',
|
||||
$sync_status['current_pending_count'],
|
||||
'woocommerce'
|
||||
),
|
||||
$sync_status['current_pending_count'],
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
'title' => '',
|
||||
'type' => 'checkbox',
|
||||
'desc' => __( 'Keep the posts and orders tables in sync (compatibility mode).', 'woocommerce' ),
|
||||
'value' => $sync_enabled,
|
||||
'desc_tip' => $sync_message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +28,22 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
|
||||
public const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled';
|
||||
private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
|
||||
public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
|
||||
public const PLACEHOLDER_ORDER_POST_TYPE = 'shop_order_placehold';
|
||||
|
||||
public const DELETED_RECORD_META_KEY = '_deleted_from';
|
||||
public const DELETED_FROM_POSTS_META_VALUE = 'posts_table';
|
||||
public const DELETED_FROM_ORDERS_META_VALUE = 'orders_table';
|
||||
|
||||
public const ORDERS_TABLE_CREATED = 'woocommerce_custom_orders_table_created';
|
||||
|
||||
private const ORDERS_SYNC_BATCH_SIZE = 250;
|
||||
|
||||
// Allowed values for $type in get_ids_of_orders_pending_sync method.
|
||||
public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0;
|
||||
public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1;
|
||||
public const ID_TYPE_DIFFERENT_UPDATE_DATE = 2;
|
||||
public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0;
|
||||
public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1;
|
||||
public const ID_TYPE_DIFFERENT_UPDATE_DATE = 2;
|
||||
public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3;
|
||||
public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4;
|
||||
|
||||
/**
|
||||
* The data store object to use.
|
||||
@@ -78,7 +86,10 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
public function __construct() {
|
||||
self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 );
|
||||
self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 );
|
||||
self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 );
|
||||
self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 );
|
||||
self::add_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 );
|
||||
|
||||
self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 );
|
||||
}
|
||||
|
||||
@@ -114,7 +125,29 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
public function check_orders_table_exists(): bool {
|
||||
$missing_tables = $this->database_util->get_missing_tables( $this->data_store->get_database_schema() );
|
||||
|
||||
return count( $missing_tables ) === 0;
|
||||
if ( count( $missing_tables ) === 0 ) {
|
||||
update_option( self::ORDERS_TABLE_CREATED, 'yes' );
|
||||
return true;
|
||||
} else {
|
||||
update_option( self::ORDERS_TABLE_CREATED, 'no' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the orders table created option. If it's not set, then it checks the orders table and set it accordingly.
|
||||
*
|
||||
* @return bool Whether orders table exists.
|
||||
*/
|
||||
public function get_table_exists(): bool {
|
||||
$table_exists = get_option( self::ORDERS_TABLE_CREATED );
|
||||
switch ( $table_exists ) {
|
||||
case 'no':
|
||||
case 'yes':
|
||||
return 'yes' === $table_exists;
|
||||
default:
|
||||
return $this->check_orders_table_exists();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +155,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
*/
|
||||
public function create_database_tables() {
|
||||
$this->database_util->dbdelta( $this->data_store->get_database_schema() );
|
||||
$this->check_orders_table_exists();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +167,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
foreach ( $table_names as $table_name ) {
|
||||
$this->database_util->drop_database_table( $table_name );
|
||||
}
|
||||
delete_option( self::ORDERS_TABLE_CREATED );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,9 +219,13 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
return (int) $pending_count;
|
||||
}
|
||||
}
|
||||
$orders_table = $this->data_store::get_orders_table_name();
|
||||
|
||||
$order_post_types = wc_get_order_types( 'cot-migration' );
|
||||
|
||||
$order_post_type_placeholder = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );
|
||||
|
||||
$orders_table = $this->data_store::get_orders_table_name();
|
||||
|
||||
if ( empty( $order_post_types ) ) {
|
||||
$this->error_logger->debug(
|
||||
sprintf(
|
||||
@@ -199,7 +238,17 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$order_post_type_placeholder = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );
|
||||
if ( ! $this->get_table_exists() ) {
|
||||
$count = $wpdb->get_var(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $wpdb->posts where post_type in ( $order_post_type_placeholder )",
|
||||
$order_post_types
|
||||
)
|
||||
// phpcs:enable
|
||||
);
|
||||
return $count;
|
||||
}
|
||||
|
||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
||||
$missing_orders_count_sql = "
|
||||
@@ -245,10 +294,32 @@ SELECT(
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$pending_count = (int) $wpdb->get_var( $sql );
|
||||
|
||||
$deleted_from_table = $this->get_current_deletion_record_meta_value();
|
||||
|
||||
$deleted_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT count(1) FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s",
|
||||
array( self::DELETED_RECORD_META_KEY, $deleted_from_table )
|
||||
)
|
||||
);
|
||||
$pending_count += $deleted_count;
|
||||
|
||||
wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count );
|
||||
return $pending_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for order deletion records based on which table is currently authoritative.
|
||||
*
|
||||
* @return string self::DELETED_FROM_ORDERS_META_VALUE if the orders table is authoritative, self::DELETED_FROM_POSTS_META_VALUE otherwise.
|
||||
*/
|
||||
private function get_current_deletion_record_meta_value() {
|
||||
return $this->custom_orders_table_is_authoritative() ?
|
||||
self::DELETED_FROM_ORDERS_META_VALUE :
|
||||
self::DELETED_FROM_POSTS_META_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the custom orders table the authoritative data source for orders currently?
|
||||
*
|
||||
@@ -266,6 +337,8 @@ SELECT(
|
||||
* ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table.
|
||||
* ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table (the corresponding post entries are placeholders).
|
||||
* ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates.
|
||||
* ID_TYPE_DELETED_FROM_ORDERS_TABLE: orders deleted from the orders table but not yet from the posts table.
|
||||
* ID_TYPE_DELETED_FROM_POSTS_TABLE: orders deleted from the posts table but not yet from the orders table.
|
||||
*
|
||||
* @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE.
|
||||
* @param int $limit Maximum number of ids to return.
|
||||
@@ -325,6 +398,10 @@ ORDER BY orders.id ASC
|
||||
);
|
||||
// phpcs:enable
|
||||
break;
|
||||
case self::ID_TYPE_DELETED_FROM_ORDERS_TABLE:
|
||||
return $this->get_deleted_order_ids( true, $limit );
|
||||
case self::ID_TYPE_DELETED_FROM_POSTS_TABLE:
|
||||
return $this->get_deleted_order_ids( false, $limit );
|
||||
default:
|
||||
throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' );
|
||||
}
|
||||
@@ -334,6 +411,31 @@ ORDER BY orders.id ASC
|
||||
return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of the orders that are marked as deleted in the orders meta table.
|
||||
*
|
||||
* @param bool $deleted_from_orders_table True to get the ids of the orders deleted from the orders table, false o get the ids of the orders deleted from the posts table.
|
||||
* @param int $limit The maximum count of orders to return.
|
||||
* @return array An array of order ids.
|
||||
*/
|
||||
private function get_deleted_order_ids( bool $deleted_from_orders_table, int $limit ) {
|
||||
global $wpdb;
|
||||
|
||||
$deleted_from_table = $this->get_current_deletion_record_meta_value();
|
||||
|
||||
$order_ids = $wpdb->get_col(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT(order_id) FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s LIMIT {$limit}",
|
||||
self::DELETED_RECORD_META_KEY,
|
||||
$deleted_from_table
|
||||
)
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
|
||||
return array_map( 'absint', $order_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all the synchronization status information,
|
||||
* because the process has been disabled by the user via settings,
|
||||
@@ -349,27 +451,124 @@ ORDER BY orders.id ASC
|
||||
* @param array $batch Batch details.
|
||||
*/
|
||||
public function process_batch( array $batch ) : void {
|
||||
if ( empty( $batch ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch = array_map( 'absint', $batch );
|
||||
|
||||
$this->order_cache_controller->temporarily_disable_orders_cache_usage();
|
||||
|
||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
||||
foreach ( $batch as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
if ( ! $order ) {
|
||||
$this->error_logger->error( "Order $id not found during batch process, skipping." );
|
||||
continue;
|
||||
$custom_orders_table_is_authoritative = $this->custom_orders_table_is_authoritative();
|
||||
$deleted_order_ids = $this->process_deleted_orders( $batch, $custom_orders_table_is_authoritative );
|
||||
$batch = array_diff( $batch, $deleted_order_ids );
|
||||
|
||||
if ( ! empty( $batch ) ) {
|
||||
if ( $custom_orders_table_is_authoritative ) {
|
||||
foreach ( $batch as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
if ( ! $order ) {
|
||||
$this->error_logger->error( "Order $id not found during batch process, skipping." );
|
||||
continue;
|
||||
}
|
||||
$data_store = $order->get_data_store();
|
||||
$data_store->backfill_post_record( $order );
|
||||
}
|
||||
$data_store = $order->get_data_store();
|
||||
$data_store->backfill_post_record( $order );
|
||||
} else {
|
||||
$this->posts_to_cot_migrator->migrate_orders( $batch );
|
||||
}
|
||||
} else {
|
||||
$this->posts_to_cot_migrator->migrate_orders( $batch );
|
||||
}
|
||||
|
||||
if ( 0 === $this->get_total_pending_count() ) {
|
||||
$this->cleanup_synchronization_state();
|
||||
$this->order_cache_controller->maybe_restore_orders_cache_usage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a batch of order ids pending synchronization and process those that were deleted, ignoring the others
|
||||
* (which will be orders that were created or modified) and returning the ids of the orders actually processed.
|
||||
*
|
||||
* @param array $batch Array of ids of order pending synchronization.
|
||||
* @param bool $custom_orders_table_is_authoritative True if the custom orders table is currently authoritative.
|
||||
* @return array Order ids that have been actually processed.
|
||||
*/
|
||||
private function process_deleted_orders( array $batch, bool $custom_orders_table_is_authoritative ): array {
|
||||
global $wpdb;
|
||||
|
||||
$deleted_from_table_name = $this->get_current_deletion_record_meta_value();
|
||||
|
||||
$data_store_for_deletion =
|
||||
$custom_orders_table_is_authoritative ?
|
||||
new \WC_Order_Data_Store_CPT() :
|
||||
wc_get_container()->get( OrdersTableDataStore::class );
|
||||
|
||||
$order_ids_as_sql_list = '(' . implode( ',', $batch ) . ')';
|
||||
|
||||
$deleted_order_ids = array();
|
||||
$meta_ids_to_delete = array();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$deletion_data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, order_id FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s AND order_id IN $order_ids_as_sql_list ORDER BY order_id DESC",
|
||||
self::DELETED_RECORD_META_KEY,
|
||||
$deleted_from_table_name
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
if ( empty( $deletion_data ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ( $deletion_data as $item ) {
|
||||
$meta_id = $item['id'];
|
||||
$order_id = $item['order_id'];
|
||||
|
||||
if ( isset( $deleted_order_ids[ $order_id ] ) ) {
|
||||
$meta_ids_to_delete[] = $meta_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $data_store_for_deletion->order_exists( $order_id ) ) {
|
||||
$this->error_logger->warning( "Order {$order_id} doesn't exist in the backup table, thus it can't be deleted" );
|
||||
$deleted_order_ids[] = $order_id;
|
||||
$meta_ids_to_delete[] = $meta_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$order = new \WC_Order();
|
||||
$order->set_id( $order_id );
|
||||
$data_store_for_deletion->read( $order );
|
||||
|
||||
$data_store_for_deletion->delete(
|
||||
$order,
|
||||
array(
|
||||
'force_delete' => true,
|
||||
'suppress_filters' => true,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $ex ) {
|
||||
$this->error_logger->error( "Couldn't delete order {$order_id} from the backup table: {$ex->getMessage()}" );
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleted_order_ids[] = $order_id;
|
||||
$meta_ids_to_delete[] = $meta_id;
|
||||
}
|
||||
|
||||
if ( ! empty( $meta_ids_to_delete ) ) {
|
||||
$order_id_rows_as_sql_list = '(' . implode( ',', $meta_ids_to_delete ) . ')';
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_orders_meta WHERE id IN {$order_id_rows_as_sql_list}" );
|
||||
}
|
||||
|
||||
return $deleted_order_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of pending records that require update.
|
||||
*
|
||||
@@ -387,17 +586,29 @@ ORDER BY orders.id ASC
|
||||
* @return array Batch of records.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ): array {
|
||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
||||
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_POSTS_TABLE, $size );
|
||||
} else {
|
||||
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $size );
|
||||
}
|
||||
$orders_table_is_authoritative = $this->custom_orders_table_is_authoritative();
|
||||
|
||||
$order_ids = $this->get_ids_of_orders_pending_sync(
|
||||
$orders_table_is_authoritative ? self::ID_TYPE_MISSING_IN_POSTS_TABLE : self::ID_TYPE_MISSING_IN_ORDERS_TABLE,
|
||||
$size
|
||||
);
|
||||
if ( count( $order_ids ) >= $size ) {
|
||||
return $order_ids;
|
||||
}
|
||||
|
||||
$order_ids = $order_ids + $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) );
|
||||
return $order_ids;
|
||||
$updated_order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) );
|
||||
$order_ids = array_merge( $order_ids, $updated_order_ids );
|
||||
if ( count( $order_ids ) >= $size ) {
|
||||
return $order_ids;
|
||||
}
|
||||
|
||||
$deleted_order_ids = $this->get_ids_of_orders_pending_sync(
|
||||
$orders_table_is_authoritative ? self::ID_TYPE_DELETED_FROM_ORDERS_TABLE : self::ID_TYPE_DELETED_FROM_POSTS_TABLE,
|
||||
$size - count( $order_ids )
|
||||
);
|
||||
$order_ids = array_merge( $order_ids, $deleted_order_ids );
|
||||
|
||||
return array_map( 'absint', $order_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,9 +660,45 @@ ORDER BY orders.id ASC
|
||||
* @param WP_Post $post The deleted post.
|
||||
*/
|
||||
private function handle_deleted_post( $postid, $post ): void {
|
||||
if ( 'shop_order' === $post->post_type && $this->data_sync_is_enabled() ) {
|
||||
$this->data_store->delete_order_data_from_custom_order_tables( $postid );
|
||||
global $wpdb;
|
||||
|
||||
$order_post_types = wc_get_order_types( 'cot-migration' );
|
||||
if ( ! in_array( $post->post_type, $order_post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->get_table_exists() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->data_sync_is_enabled() ) {
|
||||
$this->data_store->delete_order_data_from_custom_order_tables( $postid );
|
||||
} elseif ( $this->custom_orders_table_is_authoritative() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery
|
||||
if ( $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT EXISTS (SELECT id FROM {$this->data_store::get_orders_table_name()} WHERE ID=%d)
|
||||
AND NOT EXISTS (SELECT order_id FROM {$this->data_store::get_meta_table_name()} WHERE order_id=%d AND meta_key=%s AND meta_value=%s)",
|
||||
$postid,
|
||||
$postid,
|
||||
self::DELETED_RECORD_META_KEY,
|
||||
self::DELETED_FROM_POSTS_META_VALUE
|
||||
)
|
||||
)
|
||||
) {
|
||||
$wpdb->insert(
|
||||
$this->data_store::get_meta_table_name(),
|
||||
array(
|
||||
'order_id' => $postid,
|
||||
'meta_key' => self::DELETED_RECORD_META_KEY,
|
||||
'meta_value' => self::DELETED_FROM_POSTS_META_VALUE,
|
||||
)
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,6 +714,45 @@ ORDER BY orders.id ASC
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deletion of auto-draft orders in sync with WP's own auto-draft deletion.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function delete_auto_draft_orders() {
|
||||
if ( ! $this->custom_orders_table_is_authoritative() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch auto-draft orders older than 1 week.
|
||||
$to_delete = wc_get_orders(
|
||||
array(
|
||||
'date_query' => array(
|
||||
array(
|
||||
'column' => 'date_created',
|
||||
'before' => '-1 week',
|
||||
),
|
||||
),
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'status' => 'auto-draft',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $to_delete as $order ) {
|
||||
$order->delete( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after schedueld deletion of auto-draft orders has been completed.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*/
|
||||
do_action( 'woocommerce_scheduled_auto_draft_delete' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the 'woocommerce_feature_description_tip' filter.
|
||||
*
|
||||
|
||||
@@ -23,12 +23,19 @@ defined( 'ABSPATH' ) || exit;
|
||||
class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface {
|
||||
|
||||
/**
|
||||
* Order IDs for which we are checking read on sync in the current request.
|
||||
* Order IDs for which we are checking sync on read in the current request. In WooCommerce, using wc_get_order is a very common pattern, to avoid performance issues, we only sync on read once per request per order. This works because we consider out of sync orders to be an anomaly, so we don't recommend running HPOS with incompatible plugins.
|
||||
*
|
||||
* @var array.
|
||||
*/
|
||||
private static $reading_order_ids = array();
|
||||
|
||||
/**
|
||||
* Keep track of order IDs that are actively being backfilled. We use this to prevent further read on sync from add_|update_|delete_postmeta etc hooks. If we allow this, then we would end up syncing the same order multiple times as it is being backfilled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $backfilling_order_ids = array();
|
||||
|
||||
/**
|
||||
* Data stored in meta keys, but not considered "meta" for an order.
|
||||
*
|
||||
@@ -62,7 +69,6 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
'_shipping_phone',
|
||||
'_completed_date',
|
||||
'_paid_date',
|
||||
'_edit_lock',
|
||||
'_edit_last',
|
||||
'_cart_discount',
|
||||
'_cart_discount_tax',
|
||||
@@ -118,6 +124,20 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
*/
|
||||
private $error_logger;
|
||||
|
||||
/**
|
||||
* The name of the main orders table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $orders_table_name;
|
||||
|
||||
/**
|
||||
* The instance of the LegacyProxy object to use.
|
||||
*
|
||||
* @var LegacyProxy
|
||||
*/
|
||||
private $legacy_proxy;
|
||||
|
||||
/**
|
||||
* Initialize the object.
|
||||
*
|
||||
@@ -131,8 +151,11 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
final public function init( OrdersTableDataStoreMeta $data_store_meta, DatabaseUtil $database_util, LegacyProxy $legacy_proxy ) {
|
||||
$this->data_store_meta = $data_store_meta;
|
||||
$this->database_util = $database_util;
|
||||
$this->legacy_proxy = $legacy_proxy;
|
||||
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
|
||||
$this->internal_meta_keys = $this->get_internal_meta_keys();
|
||||
|
||||
$this->orders_table_name = self::get_orders_table_name();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,7 +569,18 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
return;
|
||||
}
|
||||
|
||||
$cpt_data_store->update_order_from_object( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$this->update_order_meta_from_object( $order );
|
||||
$order_class = get_class( $order );
|
||||
$post_order = new $order_class();
|
||||
$post_order->set_id( $order->get_id() );
|
||||
$cpt_data_store->read( $post_order );
|
||||
|
||||
// This compares the order data to the post data and set changes array for props that are changed.
|
||||
$post_order->set_props( $order->get_data() );
|
||||
|
||||
$cpt_data_store->update_order_from_object( $post_order );
|
||||
|
||||
foreach ( $cpt_data_store->get_internal_data_store_key_getters() as $key => $getter_name ) {
|
||||
if (
|
||||
is_callable( array( $cpt_data_store, "set_$getter_name" ) ) &&
|
||||
@@ -564,6 +598,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
);
|
||||
}
|
||||
}
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -746,6 +781,47 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
$this->set_stock_reduced( $order, $set );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token ids for an order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
public function get_payment_token_ids( $order ) {
|
||||
/**
|
||||
* We don't store _payment_tokens in props to preserve backward compatibility. In CPT data store, `_payment_tokens` is always fetched directly from DB instead of from prop.
|
||||
*/
|
||||
$payment_tokens = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
|
||||
if ( $payment_tokens ) {
|
||||
$payment_tokens = $payment_tokens[0]->meta_value;
|
||||
}
|
||||
if ( ! $payment_tokens && version_compare( $order->get_version(), '8.0.0', '<' ) ) {
|
||||
// Before 8.0 we were incorrectly storing payment_tokens in the order meta. So we need to check there too.
|
||||
$payment_tokens = get_post_meta( $order->get_id(), '_payment_tokens', true );
|
||||
}
|
||||
return array_filter( (array) $payment_tokens );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
$meta = new \WC_Meta_Data();
|
||||
$meta->key = '_payment_tokens';
|
||||
$meta->value = $token_ids;
|
||||
$existing_meta = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
|
||||
if ( $existing_meta ) {
|
||||
$existing_meta = $existing_meta[0];
|
||||
$meta->id = $existing_meta->id;
|
||||
$this->data_store_meta->update_meta( $order, $meta );
|
||||
} else {
|
||||
$this->data_store_meta->add_meta( $order, $meta );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amount already refunded.
|
||||
*
|
||||
@@ -979,6 +1055,29 @@ WHERE
|
||||
return $type[ $order_id ] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT EXISTS (SELECT id FROM {$this->orders_table_name} WHERE id=%d)",
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to read an order from custom tables.
|
||||
*
|
||||
@@ -1011,8 +1110,8 @@ WHERE
|
||||
return;
|
||||
}
|
||||
|
||||
$data_sync_enabled = $data_synchronizer->data_sync_is_enabled() && 0 === $data_synchronizer->get_current_orders_pending_sync_count_cached();
|
||||
$load_posts_for = array_diff( $order_ids, self::$reading_order_ids );
|
||||
$data_sync_enabled = $data_synchronizer->data_sync_is_enabled();
|
||||
$load_posts_for = array_diff( $order_ids, array_merge( self::$reading_order_ids, self::$backfilling_order_ids ) );
|
||||
$post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();
|
||||
|
||||
foreach ( $data as $order_data ) {
|
||||
@@ -1301,8 +1400,7 @@ WHERE
|
||||
* @return void
|
||||
*/
|
||||
private function log_diff( array $diff ): void {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is a log function.
|
||||
$this->error_logger->notice( 'Diff found: ' . print_r( $diff, true ) );
|
||||
$this->error_logger->notice( 'Diff found: ' . wp_json_encode( $diff, JSON_PRETTY_PRINT ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1328,7 +1426,7 @@ WHERE
|
||||
* @param \WC_Abstract_Order $order The order object.
|
||||
* @param object $order_data A row of order data from the database.
|
||||
*/
|
||||
private function set_order_props_from_data( &$order, $order_data ) {
|
||||
protected function set_order_props_from_data( &$order, $order_data ) {
|
||||
foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mapping ) {
|
||||
foreach ( $column_mapping as $column_name => $prop_details ) {
|
||||
if ( ! isset( $prop_details['name'] ) ) {
|
||||
@@ -1339,11 +1437,31 @@ WHERE
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'date' === $prop_details['type'] ) {
|
||||
$prop_value = $this->string_to_timestamp( $prop_value );
|
||||
}
|
||||
try {
|
||||
if ( 'date' === $prop_details['type'] ) {
|
||||
$prop_value = $this->string_to_timestamp( $prop_value );
|
||||
}
|
||||
|
||||
$this->set_order_prop( $order, $prop_details['name'], $prop_value );
|
||||
$this->set_order_prop( $order, $prop_details['name'], $prop_value );
|
||||
} catch ( \Exception $e ) {
|
||||
$order_id = $order->get_id();
|
||||
$this->error_logger->warning(
|
||||
sprintf(
|
||||
/* translators: %1$d = peoperty name, %2$d = order ID, %3$s = error message. */
|
||||
__( 'Error when setting property \'%1$s\' for order %2$d: %3$s', 'woocommerce' ),
|
||||
$prop_details['name'],
|
||||
$order_id,
|
||||
$e->getMessage()
|
||||
),
|
||||
array(
|
||||
'exception_code' => $e->getCode(),
|
||||
'exception_msg' => $e->getMessage(),
|
||||
'origin' => __METHOD__,
|
||||
'order_id' => $order_id,
|
||||
'property_name' => $prop_details['name'],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1598,8 +1716,11 @@ FROM $order_meta_table
|
||||
if ( 'create' === $context ) {
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
||||
'post_status' => 'draft',
|
||||
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
||||
'post_status' => 'draft',
|
||||
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
|
||||
'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() ),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1634,6 +1755,84 @@ FROM $order_meta_table
|
||||
|
||||
$changes = $order->get_changes();
|
||||
$this->update_address_index_meta( $order, $changes );
|
||||
$default_taxonomies = $this->init_default_taxonomies( $order, array() );
|
||||
$this->set_custom_taxonomies( $order, $default_taxonomies );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default taxonomies for the order.
|
||||
*
|
||||
* Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set default taxonomies is not filterable, we have to re-implement it.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $sanitized_tax_input Sanitized taxonomy input.
|
||||
*
|
||||
* @return array Sanitized tax input with default taxonomies.
|
||||
*/
|
||||
public function init_default_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) {
|
||||
if ( 'auto-draft' === $order->get_status() ) {
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
foreach ( get_object_taxonomies( $order->get_type(), 'object' ) as $taxonomy => $tax_object ) {
|
||||
if ( empty( $tax_object->default_term ) ) {
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
// Filter out empty terms.
|
||||
if ( isset( $sanitized_tax_input[ $taxonomy ] ) && is_array( $sanitized_tax_input[ $taxonomy ] ) ) {
|
||||
$sanitized_tax_input[ $taxonomy ] = array_filter( $sanitized_tax_input[ $taxonomy ] );
|
||||
}
|
||||
|
||||
// Passed custom taxonomy list overwrites the existing list if not empty.
|
||||
$terms = wp_get_object_terms( $order->get_id(), $taxonomy, array( 'fields' => 'ids' ) );
|
||||
if ( ! empty( $terms ) && empty( $sanitized_tax_input[ $taxonomy ] ) ) {
|
||||
$sanitized_tax_input[ $taxonomy ] = $terms;
|
||||
}
|
||||
|
||||
if ( empty( $sanitized_tax_input[ $taxonomy ] ) ) {
|
||||
$default_term_id = get_option( 'default_term_' . $taxonomy );
|
||||
if ( ! empty( $default_term_id ) ) {
|
||||
$sanitized_tax_input[ $taxonomy ] = array( (int) $default_term_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom taxonomies for the order.
|
||||
*
|
||||
* Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set custom taxonomies is not filterable, we have to re-implement it.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $sanitized_tax_input Sanitized taxonomy input.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_custom_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) {
|
||||
if ( empty( $sanitized_tax_input ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $sanitized_tax_input as $taxonomy => $tags ) {
|
||||
$taxonomy_obj = get_taxonomy( $taxonomy );
|
||||
|
||||
if ( ! $taxonomy_obj ) {
|
||||
/* translators: %s: Taxonomy name. */
|
||||
_doing_it_wrong( __FUNCTION__, esc_html( sprintf( __( 'Invalid taxonomy: %s.', 'woocommerce' ), $taxonomy ) ), '7.9.0' );
|
||||
continue;
|
||||
}
|
||||
|
||||
// array = hierarchical, string = non-hierarchical.
|
||||
if ( is_array( $tags ) ) {
|
||||
$tags = array_filter( $tags );
|
||||
}
|
||||
|
||||
if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
|
||||
wp_set_post_terms( $order->get_id(), $tags, $taxonomy );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1659,8 +1858,8 @@ FROM $order_meta_table
|
||||
if ( $row ) {
|
||||
$result[] = array(
|
||||
'table' => self::get_orders_table_name(),
|
||||
'data' => array_merge( $row['data'], array( 'id' => $order->get_id() ) ),
|
||||
'format' => array_merge( $row['format'], array( 'id' => '%d' ) ),
|
||||
'data' => array_merge( $row['data'], array( 'id' => $order->get_id(), 'type' => $order->get_type() ) ),
|
||||
'format' => array_merge( $row['format'], array( 'id' => '%d', 'type' => '%s' ) ),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1729,10 +1928,8 @@ FROM $order_meta_table
|
||||
protected function get_db_row_from_order( $order, $column_mapping, $only_changes = false ) {
|
||||
$changes = $only_changes ? $order->get_changes() : array_merge( $order->get_data(), $order->get_changes() );
|
||||
|
||||
$changes['type'] = $order->get_type();
|
||||
|
||||
// Make sure 'status' is correct.
|
||||
if ( array_key_exists( 'status', $column_mapping ) ) {
|
||||
// Make sure 'status' is correctly prefixed.
|
||||
if ( array_key_exists( 'status', $column_mapping ) && array_key_exists( 'status', $changes ) ) {
|
||||
$changes['status'] = $this->get_post_status( $order );
|
||||
}
|
||||
|
||||
@@ -1773,19 +1970,31 @@ FROM $order_meta_table
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $args['force_delete'] ) ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
array(
|
||||
'force_delete' => false,
|
||||
'suppress_filters' => false,
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Fires immediately before an order is deleted from the database.
|
||||
*
|
||||
* @since 7.1.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', $order_id, $order );
|
||||
$do_filters = ! $args['suppress_filters'];
|
||||
|
||||
$this->upshift_child_orders( $order );
|
||||
if ( $args['force_delete'] ) {
|
||||
|
||||
if ( $do_filters ) {
|
||||
/**
|
||||
* Fires immediately before an order is deleted from the database.
|
||||
*
|
||||
* @since 7.1.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', $order_id, $order );
|
||||
}
|
||||
|
||||
$this->upshift_or_delete_child_orders( $order );
|
||||
$this->delete_order_data_from_custom_order_tables( $order_id );
|
||||
$this->delete_items( $order );
|
||||
|
||||
@@ -1797,49 +2006,140 @@ FROM $order_meta_table
|
||||
*
|
||||
* In other words, we do not delete the post record when HPOS table is authoritative and synchronization is disabled but post record is a full record and not just a placeholder, because it implies that the order was created before HPOS was enabled.
|
||||
*/
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() || get_post_type( $order_id ) === 'shop_order_placehold' ) {
|
||||
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
|
||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
||||
wp_delete_post( $order_id );
|
||||
$orders_table_is_authoritative = $order->get_data_store()->get_current_class_name() === self::class;
|
||||
|
||||
if ( $orders_table_is_authoritative ) {
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
|
||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
||||
wp_delete_post( $order_id );
|
||||
} else {
|
||||
$this->handle_order_deletion_with_sync_disabled( $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
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', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Fires immediately before an order is trashed.
|
||||
*
|
||||
* @since 7.1.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_trash_order', $order_id, $order );
|
||||
if ( $do_filters ) {
|
||||
/**
|
||||
* Fires immediately before an order is trashed.
|
||||
*
|
||||
* @since 7.1.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', $order_id, $order );
|
||||
}
|
||||
|
||||
$this->trash_order( $order );
|
||||
|
||||
do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
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', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set child orders to the parent order's parent.
|
||||
* Handles the deletion of an order from the orders table when sync is disabled:
|
||||
*
|
||||
* If the corresponding row in the posts table is of placeholder type,
|
||||
* it's just deleted; otherwise a "deleted_from" record is created in the meta table
|
||||
* and the sync process will detect these and take care of deleting the appropriate post records.
|
||||
*
|
||||
* @param int $order_id Th id of the order that has been deleted from the orders table.
|
||||
* @return void
|
||||
*/
|
||||
protected function handle_order_deletion_with_sync_disabled( $order_id ): void {
|
||||
global $wpdb;
|
||||
|
||||
$post_type = $wpdb->get_var(
|
||||
$wpdb->prepare( "SELECT post_type FROM {$wpdb->posts} WHERE ID=%d", $order_id )
|
||||
);
|
||||
|
||||
if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === $post_type ) {
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->posts} WHERE ID=%d OR post_parent=%d",
|
||||
$order_id,
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// phpcs:disable WordPress.DB.SlowDBQuery
|
||||
$wpdb->insert(
|
||||
self::get_meta_table_name(),
|
||||
array(
|
||||
'order_id' => $order_id,
|
||||
'meta_key' => DataSynchronizer::DELETED_RECORD_META_KEY,
|
||||
'meta_value' => DataSynchronizer::DELETED_FROM_ORDERS_META_VALUE,
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.SlowDBQuery
|
||||
|
||||
// Note that at this point upshift_or_delete_child_orders will already have been invoked,
|
||||
// thus all the child orders either still exist but have a different parent id,
|
||||
// or have been deleted and got their own deletion record already.
|
||||
// So there's no need to do anything about them.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent id of child orders to the parent order's parent if the post type
|
||||
* for the order is hierarchical, just delete the child orders otherwise.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function upshift_child_orders( $order ) {
|
||||
private function upshift_or_delete_child_orders( $order ) : void {
|
||||
global $wpdb;
|
||||
$order_table = self::get_orders_table_name();
|
||||
$order_parent = $order->get_parent_id();
|
||||
$wpdb->update(
|
||||
$order_table,
|
||||
array( 'parent_order_id' => $order_parent ),
|
||||
array( 'parent_order_id' => $order->get_id() ),
|
||||
array( '%d' ),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
$order_table = self::get_orders_table_name();
|
||||
$order_parent_id = $order->get_parent_id();
|
||||
|
||||
if ( $this->legacy_proxy->call_function( 'is_post_type_hierarchical', $order->get_type() ) ) {
|
||||
$wpdb->update(
|
||||
$order_table,
|
||||
array( 'parent_order_id' => $order_parent_id ),
|
||||
array( 'parent_order_id' => $order->get_id() ),
|
||||
array( '%d' ),
|
||||
array( '%d' )
|
||||
);
|
||||
} else {
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$child_order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM $order_table WHERE parent_order_id=%d",
|
||||
$order->get_id()
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
foreach ( $child_order_ids as $child_order_id ) {
|
||||
$child_order = wc_get_order( $child_order_id );
|
||||
if ( $child_order ) {
|
||||
$child_order->delete( true );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1861,16 +2161,6 @@ FROM $order_meta_table
|
||||
'_wp_trash_meta_time' => time(),
|
||||
);
|
||||
|
||||
foreach ( $trash_metadata as $meta_key => $meta_value ) {
|
||||
$this->add_meta(
|
||||
$order,
|
||||
(object) array(
|
||||
'key' => $meta_key,
|
||||
'value' => $meta_value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
self::get_orders_table_name(),
|
||||
array(
|
||||
@@ -1884,6 +2174,16 @@ FROM $order_meta_table
|
||||
|
||||
$order->set_status( 'trash' );
|
||||
|
||||
foreach ( $trash_metadata as $meta_key => $meta_value ) {
|
||||
$this->add_meta(
|
||||
$order,
|
||||
(object) array(
|
||||
'key' => $meta_key,
|
||||
'value' => $meta_value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
wp_trash_post( $order->get_id() );
|
||||
@@ -2015,6 +2315,11 @@ FROM $order_meta_table
|
||||
|
||||
$this->persist_save( $order );
|
||||
|
||||
// Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility.
|
||||
if ( 'auto-draft' === $order->get_status( 'edit') ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a new order is created.
|
||||
*
|
||||
@@ -2047,15 +2352,22 @@ FROM $order_meta_table
|
||||
$order->set_date_created( time() );
|
||||
}
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
if ( ! $order->get_date_modified( 'edit' ) ) {
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
$this->persist_order_to_db( $order, $force_all_fields );
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
|
||||
$order->save_meta_data();
|
||||
$order->apply_changes();
|
||||
|
||||
if ( $backfill ) {
|
||||
$this->maybe_backfill_post_record( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
|
||||
$this->maybe_backfill_post_record( $r_order );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
$this->clear_caches( $order );
|
||||
}
|
||||
@@ -2066,6 +2378,9 @@ FROM $order_meta_table
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function update( &$order ) {
|
||||
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
|
||||
$changes = $order->get_changes();
|
||||
|
||||
// Before updating, ensure date paid is set if missing.
|
||||
if (
|
||||
! $order->get_date_paid( 'edit' )
|
||||
@@ -2099,6 +2414,18 @@ FROM $order_meta_table
|
||||
$order->apply_changes();
|
||||
$this->clear_caches( $order );
|
||||
|
||||
// For backwards compatibility, moving an auto-draft order to a valid status triggers the 'woocommerce_new_order' hook.
|
||||
if ( ! empty( $changes['status'] ) && 'auto-draft' === $previous_status ) {
|
||||
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
return;
|
||||
}
|
||||
|
||||
// For backwards compat with CPT, trashing/untrashing and changing previously datastore-level props does not trigger the update hook.
|
||||
if ( ( ! empty( $changes['status'] ) && in_array( 'trash', array( $changes['status'], $previous_status ), true ) )
|
||||
|| ! array_diff_key( $changes, array_flip( $this->get_post_data_store_for_backfill()->get_internal_data_store_key_getters() ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
}
|
||||
|
||||
@@ -2130,19 +2457,33 @@ FROM $order_meta_table
|
||||
$changes = $order->get_changes();
|
||||
|
||||
if ( ! isset( $changes['date_modified'] ) ) {
|
||||
$order->set_date_modified( time() );
|
||||
}
|
||||
|
||||
if ( $backfill ) {
|
||||
$this->maybe_backfill_post_record( $order );
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
$this->persist_order_to_db( $order );
|
||||
$order->save_meta_data();
|
||||
|
||||
if ( $backfill ) {
|
||||
$this->clear_caches( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
|
||||
$this->maybe_backfill_post_record( $r_order );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check whether to backfill post record.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_backfill_post_record() {
|
||||
$data_sync = wc_get_container()->get( DataSynchronizer::class );
|
||||
return $data_sync->data_sync_is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to decide whether to backfill post record.
|
||||
*
|
||||
@@ -2151,8 +2492,7 @@ FROM $order_meta_table
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_backfill_post_record( $order ) {
|
||||
$data_sync = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_sync->data_sync_is_enabled() ) {
|
||||
if ( $this->should_backfill_post_record() ) {
|
||||
$this->backfill_post_record( $order );
|
||||
}
|
||||
}
|
||||
@@ -2344,6 +2684,10 @@ FROM $order_meta_table
|
||||
$operational_data_table_name = $this->get_operational_data_table_name();
|
||||
$meta_table = $this->get_meta_table_name();
|
||||
|
||||
$max_index_length = $this->database_util->get_max_index_length();
|
||||
$composite_meta_value_index_length = max( $max_index_length - 8 - 100 - 1, 20 ); // 8 for order_id, 100 for meta_key, 10 minimum for meta_value.
|
||||
$composite_customer_id_email_length = max( $max_index_length - 20, 20 ); // 8 for customer_id, 20 minimum for email.
|
||||
|
||||
$sql = "
|
||||
CREATE TABLE $orders_table_name (
|
||||
id bigint(20) unsigned,
|
||||
@@ -2366,9 +2710,9 @@ CREATE TABLE $orders_table_name (
|
||||
PRIMARY KEY (id),
|
||||
KEY status (status),
|
||||
KEY date_created (date_created_gmt),
|
||||
KEY customer_id_billing_email (customer_id, billing_email),
|
||||
KEY billing_email (billing_email),
|
||||
KEY type_status (type, status),
|
||||
KEY customer_id_billing_email (customer_id, billing_email({$composite_customer_id_email_length})),
|
||||
KEY billing_email (billing_email($max_index_length)),
|
||||
KEY type_status_date (type, status, date_created_gmt),
|
||||
KEY parent_order_id (parent_order_id),
|
||||
KEY date_updated (date_updated_gmt)
|
||||
) $collate;
|
||||
@@ -2389,7 +2733,7 @@ CREATE TABLE $addresses_table_name (
|
||||
phone varchar(100) null,
|
||||
KEY order_id (order_id),
|
||||
UNIQUE KEY address_type_order_id (address_type, order_id),
|
||||
KEY email (email),
|
||||
KEY email (email($max_index_length)),
|
||||
KEY phone (phone)
|
||||
) $collate;
|
||||
CREATE TABLE $operational_data_table_name (
|
||||
@@ -2406,10 +2750,10 @@ CREATE TABLE $operational_data_table_name (
|
||||
order_stock_reduced tinyint(1) NULL,
|
||||
date_paid_gmt datetime NULL,
|
||||
date_completed_gmt datetime NULL,
|
||||
shipping_tax_amount decimal(26, 8) NULL,
|
||||
shipping_total_amount decimal(26, 8) NULL,
|
||||
discount_tax_amount decimal(26, 8) NULL,
|
||||
discount_total_amount decimal(26, 8) NULL,
|
||||
shipping_tax_amount decimal(26,8) NULL,
|
||||
shipping_total_amount decimal(26,8) NULL,
|
||||
discount_tax_amount decimal(26,8) NULL,
|
||||
discount_total_amount decimal(26,8) NULL,
|
||||
recorded_sales tinyint(1) NULL,
|
||||
UNIQUE KEY order_id (order_id),
|
||||
KEY order_key (order_key)
|
||||
@@ -2419,8 +2763,8 @@ CREATE TABLE $meta_table (
|
||||
order_id bigint(20) unsigned null,
|
||||
meta_key varchar(255),
|
||||
meta_value text null,
|
||||
KEY meta_key_value (meta_key, meta_value(100)),
|
||||
KEY order_id_meta_key_meta_value (order_id, meta_key, meta_value(100))
|
||||
KEY meta_key_value (meta_key(100), meta_value($composite_meta_value_index_length)),
|
||||
KEY order_id_meta_key_meta_value (order_id, meta_key(100), meta_value($composite_meta_value_index_length))
|
||||
) $collate;
|
||||
";
|
||||
|
||||
@@ -2441,16 +2785,28 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Deletes meta based on meta ID.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing at least ->id).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing at least ->id).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_meta( &$object, $meta ) {
|
||||
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
|
||||
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
|
||||
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
|
||||
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
|
||||
if ( $db_meta ) {
|
||||
$meta->key = $db_meta->meta_key;
|
||||
$meta->value = $db_meta->meta_value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $delete_meta;
|
||||
@@ -2459,16 +2815,20 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Add new piece of meta.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing ->key and ->value).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing ->key and ->value).
|
||||
*
|
||||
* @return int|bool meta ID or false on failure
|
||||
*/
|
||||
public function add_meta( &$object, $meta ) {
|
||||
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
|
||||
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
|
||||
$meta->id = $add_meta;
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
add_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $add_meta;
|
||||
@@ -2477,18 +2837,58 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Update meta.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing ->id, ->key and ->value).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing ->id, ->key and ->value).
|
||||
*
|
||||
* @return bool
|
||||
* @return
|
||||
*/
|
||||
public function update_meta( &$object, $meta ) {
|
||||
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
|
||||
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
update_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $update_meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform after meta change operations, including updating the date_modified field, clearing caches and applying changes.
|
||||
*
|
||||
* @param WC_Abstract_Order $order Order object.
|
||||
* @param \WC_Meta_Data $meta Metadata object.
|
||||
*
|
||||
* @return bool True if changes were applied, false otherwise.
|
||||
*/
|
||||
protected function after_meta_change( &$order, $meta ) {
|
||||
method_exists( $meta, 'apply_changes' ) && $meta->apply_changes();
|
||||
$this->clear_caches( $order );
|
||||
|
||||
// Prevent this happening multiple time in same request.
|
||||
if ( $this->should_save_after_meta_change( $order ) ) {
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
$order->save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check whether the modified date needs to be updated after a meta save.
|
||||
*
|
||||
* This method prevents order->save() call multiple times in the same request after any meta update by checking if:
|
||||
* 1. Order modified date is already the current date, no updates needed in this case.
|
||||
* 2. If there are changes already queued for order object, then we don't need to update the modified date as it will be updated ina subsequent save() call.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*
|
||||
* @return bool Whether the modified date needs to be updated.
|
||||
*/
|
||||
private function should_save_after_meta_change( $order ) {
|
||||
$current_date_time = new \WC_DateTime( current_time( 'mysql', 1 ), new \DateTimeZone( 'GMT' ) );
|
||||
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class OrdersTableQuery {
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $results = array();
|
||||
private $orders = array();
|
||||
|
||||
/**
|
||||
* Final SQL query to run after processing of args.
|
||||
@@ -171,6 +171,13 @@ class OrdersTableQuery {
|
||||
*/
|
||||
private $order_datastore = null;
|
||||
|
||||
/**
|
||||
* Whether to run filters to modify the query or not.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $suppress_filters = false;
|
||||
|
||||
/**
|
||||
* Sets up and runs the query after processing arguments.
|
||||
*
|
||||
@@ -183,6 +190,9 @@ class OrdersTableQuery {
|
||||
$this->tables = $this->order_datastore::get_all_table_names_with_id();
|
||||
$this->mappings = $this->order_datastore->get_all_order_column_mappings();
|
||||
|
||||
$this->suppress_filters = array_key_exists( 'suppress_filters', $args ) ? (bool) $args['suppress_filters'] : false;
|
||||
unset( $args['suppress_filters'] );
|
||||
|
||||
$this->args = $args;
|
||||
|
||||
// TODO: args to be implemented.
|
||||
@@ -686,7 +696,7 @@ class OrdersTableQuery {
|
||||
}
|
||||
|
||||
// ORDER BY.
|
||||
$orderby = $this->orderby ? ( 'ORDER BY ' . implode( ', ', $this->orderby ) ) : '';
|
||||
$orderby = $this->orderby ? implode( ', ', $this->orderby ) : '';
|
||||
|
||||
// LIMITS.
|
||||
$limits = '';
|
||||
@@ -698,9 +708,58 @@ class OrdersTableQuery {
|
||||
}
|
||||
|
||||
// GROUP BY.
|
||||
$groupby = $this->groupby ? 'GROUP BY ' . implode( ', ', (array) $this->groupby ) : '';
|
||||
$groupby = $this->groupby ? implode( ', ', (array) $this->groupby ) : '';
|
||||
|
||||
$pieces = compact( 'fields', 'join', 'where', 'groupby', 'orderby', 'limits' );
|
||||
|
||||
if ( ! $this->suppress_filters ) {
|
||||
/**
|
||||
* Filters all query clauses at once.
|
||||
* Covers the fields (SELECT), JOIN, WHERE, GROUP BY, ORDER BY, and LIMIT clauses.
|
||||
*
|
||||
* @since 7.9.0
|
||||
*
|
||||
* @param string[] $clauses {
|
||||
* Associative array of the clauses for the query.
|
||||
*
|
||||
* @type string $fields The SELECT clause of the query.
|
||||
* @type string $join The JOIN clause of the query.
|
||||
* @type string $where The WHERE clause of the query.
|
||||
* @type string $groupby The GROUP BY clause of the query.
|
||||
* @type string $orderby The ORDER BY clause of the query.
|
||||
* @type string $limits The LIMIT clause of the query.
|
||||
* }
|
||||
* @param OrdersTableQuery $query The OrdersTableQuery instance (passed by reference).
|
||||
* @param array $args Query args.
|
||||
*/
|
||||
$clauses = (array) apply_filters_ref_array( 'woocommerce_orders_table_query_clauses', array( $pieces, &$this, $this->args ) );
|
||||
|
||||
$fields = $clauses['fields'] ?? '';
|
||||
$join = $clauses['join'] ?? '';
|
||||
$where = $clauses['where'] ?? '';
|
||||
$groupby = $clauses['groupby'] ?? '';
|
||||
$orderby = $clauses['orderby'] ?? '';
|
||||
$limits = $clauses['limits'] ?? '';
|
||||
}
|
||||
|
||||
$groupby = $groupby ? ( 'GROUP BY ' . $groupby ) : '';
|
||||
$orderby = $orderby ? ( 'ORDER BY ' . $orderby ) : '';
|
||||
|
||||
$this->sql = "SELECT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits";
|
||||
|
||||
if ( ! $this->suppress_filters ) {
|
||||
/**
|
||||
* Filters the completed SQL query.
|
||||
*
|
||||
* @since 7.9.0
|
||||
*
|
||||
* @param string $sql The complete SQL query.
|
||||
* @param OrdersTableQuery $query The OrdersTableQuery instance (passed by reference).
|
||||
* @param array $args Query args.
|
||||
*/
|
||||
$this->sql = apply_filters_ref_array( 'woocommerce_orders_table_query_sql', array( $this->sql, &$this, $this->args ) );
|
||||
}
|
||||
|
||||
$this->build_count_query( $fields, $join, $where, $groupby );
|
||||
}
|
||||
|
||||
@@ -1156,7 +1215,7 @@ class OrdersTableQuery {
|
||||
return $this->max_num_pages;
|
||||
case 'posts':
|
||||
case 'orders':
|
||||
return $this->results;
|
||||
return $this->orders;
|
||||
case 'request':
|
||||
return $this->sql;
|
||||
default:
|
||||
|
||||
@@ -6,11 +6,25 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use WC_Meta_Data;
|
||||
|
||||
/**
|
||||
* Class OrdersTableRefundDataStore.
|
||||
*/
|
||||
class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||
|
||||
/**
|
||||
* Data stored in meta keys, but not considered "meta" for refund.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $internal_meta_keys = array(
|
||||
'_refund_amount',
|
||||
'_refund_reason',
|
||||
'_refunded_by',
|
||||
'_refunded_payment',
|
||||
);
|
||||
|
||||
/**
|
||||
* We do not have and use all the getters and setters from OrderTableDataStore, so we only select the props we actually need.
|
||||
*
|
||||
@@ -66,54 +80,46 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||
$this->delete_order_data_from_custom_order_tables( $refund_id );
|
||||
$refund->set_id( 0 );
|
||||
|
||||
// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
|
||||
if ( ! is_a( $refund->get_data_store(), self::class ) ) {
|
||||
return;
|
||||
}
|
||||
$orders_table_is_authoritative = $refund->get_data_store()->get_current_class_name() === self::class;
|
||||
|
||||
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
|
||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
||||
wp_delete_post( $refund_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a refund object from custom tables.
|
||||
*
|
||||
* @param \WC_Abstract_Order $refund Refund object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function read( &$refund ) {
|
||||
parent::read( $refund );
|
||||
$this->set_refund_props( $refund );
|
||||
}
|
||||
|
||||
/**
|
||||
* Read multiple refund objects from custom tables.
|
||||
*
|
||||
* @param \WC_Order $refunds Refund objects.
|
||||
*/
|
||||
public function read_multiple( &$refunds ) {
|
||||
parent::read_multiple( $refunds );
|
||||
foreach ( $refunds as $refund ) {
|
||||
$this->set_refund_props( $refund );
|
||||
if ( $orders_table_is_authoritative ) {
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
|
||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
||||
wp_delete_post( $refund_id );
|
||||
} else {
|
||||
$this->handle_order_deletion_with_sync_disabled( $refund_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set refund props.
|
||||
*
|
||||
* @param \WC_Order $refund Refund object.
|
||||
* @param \WC_Order_Refund $refund Refund object.
|
||||
* @param object $data DB data object.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
private function set_refund_props( $refund ) {
|
||||
$refund->set_props(
|
||||
array(
|
||||
'amount' => $refund->get_meta( '_refund_amount', true ),
|
||||
'refunded_by' => $refund->get_meta( '_refunded_by', true ),
|
||||
'refunded_payment' => wc_string_to_bool( $refund->get_meta( '_refunded_payment', true ) ),
|
||||
'reason' => $refund->get_meta( '_refund_reason', true ),
|
||||
)
|
||||
);
|
||||
protected function set_order_props_from_data( &$refund, $data ) {
|
||||
parent::set_order_props_from_data( $refund, $data );
|
||||
foreach ( $data->meta_data as $meta ) {
|
||||
switch ( $meta->meta_key ) {
|
||||
case '_refund_amount':
|
||||
$refund->set_amount( $meta->meta_value );
|
||||
break;
|
||||
case '_refunded_by':
|
||||
$refund->set_refunded_by( $meta->meta_value );
|
||||
break;
|
||||
case '_refunded_payment':
|
||||
$refund->set_refunded_payment( wc_string_to_bool( $meta->meta_value ) );
|
||||
break;
|
||||
case '_refund_reason':
|
||||
$refund->set_reason( $meta->meta_value );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,8 +161,17 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||
|
||||
$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' );
|
||||
$refund->update_meta_data( $meta_key, $value );
|
||||
$meta_object = new WC_Meta_Data();
|
||||
$meta_object->key = $meta_key;
|
||||
$meta_object->value = $refund->{"get_$prop"}( 'edit' );
|
||||
$existing_meta = $this->data_store_meta->get_metadata_by_key( $refund, $meta_key );
|
||||
if ( $existing_meta ) {
|
||||
$existing_meta = $existing_meta[0];
|
||||
$meta_object->id = $existing_meta->id;
|
||||
$this->update_meta( $refund, $meta_object );
|
||||
} else {
|
||||
$this->add_meta( $refund, $meta_object );
|
||||
}
|
||||
$updated_props[] = $prop;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ class OrdersTableSearchQuery {
|
||||
* @param OrdersTableQuery $query The order query object.
|
||||
*/
|
||||
public function __construct( OrdersTableQuery $query ) {
|
||||
$this->query = $query;
|
||||
$this->search_term = "'" . esc_sql( '%' . urldecode( $query->get( 's' ) ) . '%' ) . "'";
|
||||
global $wpdb;
|
||||
$this->query = $query;
|
||||
$this->search_term = esc_sql( '%' . $wpdb->esc_like( urldecode( $query->get( 's' ) ) ) . '%' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +79,7 @@ class OrdersTableSearchQuery {
|
||||
* @return string
|
||||
*/
|
||||
private function generate_where(): string {
|
||||
global $wpdb;
|
||||
$where = '';
|
||||
$possible_order_id = (string) absint( $this->query->get( 's' ) );
|
||||
$order_table = $this->query->get_table_name( 'orders' );
|
||||
@@ -89,10 +91,13 @@ class OrdersTableSearchQuery {
|
||||
|
||||
$meta_sub_query = $this->generate_where_for_meta_table();
|
||||
|
||||
$where .= "
|
||||
search_query_items.order_item_name LIKE $this->search_term
|
||||
$where .= $wpdb->prepare(
|
||||
"
|
||||
search_query_items.order_item_name LIKE %s
|
||||
OR `$order_table`.id IN ( $meta_sub_query )
|
||||
";
|
||||
",
|
||||
$this->search_term
|
||||
);
|
||||
|
||||
return " ( $where ) ";
|
||||
}
|
||||
@@ -107,15 +112,19 @@ class OrdersTableSearchQuery {
|
||||
* @return string The where clause for meta table.
|
||||
*/
|
||||
private function generate_where_for_meta_table(): string {
|
||||
global $wpdb;
|
||||
$meta_table = $this->query->get_table_name( 'meta' );
|
||||
$meta_fields = $this->get_meta_fields_to_be_searched();
|
||||
return "
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT search_query_meta.order_id
|
||||
FROM $meta_table as search_query_meta
|
||||
WHERE search_query_meta.meta_key IN ( $meta_fields )
|
||||
AND search_query_meta.meta_value LIKE $this->search_term
|
||||
AND search_query_meta.meta_value LIKE %s
|
||||
GROUP BY search_query_meta.order_id
|
||||
";
|
||||
",
|
||||
$this->search_term
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,15 +25,46 @@ class Definition extends BaseDefinition {
|
||||
* @return object
|
||||
*/
|
||||
protected function resolveClass( string $concrete ) {
|
||||
$instance = new $concrete();
|
||||
$this->invokeInit( $instance );
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke methods on resolved instance, including 'init'.
|
||||
*
|
||||
* @param object $instance The concrete to invoke methods on.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function invokeMethods( $instance ) {
|
||||
$this->invokeInit( $instance );
|
||||
parent::invokeMethods( $instance );
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the 'init' method on a resolved object.
|
||||
*
|
||||
* Constructor injection causes backwards compatibility problems
|
||||
* so we will rely on method injection via an internal method.
|
||||
*
|
||||
* @param object $instance The resolved object.
|
||||
* @return void
|
||||
*/
|
||||
private function invokeInit( $instance ) {
|
||||
$resolved = $this->resolveArguments( $this->arguments );
|
||||
$concrete = new $concrete();
|
||||
|
||||
// Constructor injection causes backwards compatibility problems
|
||||
// so we will rely on method injection via an internal method.
|
||||
if ( method_exists( $concrete, static::INJECTION_METHOD ) ) {
|
||||
call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved );
|
||||
if ( method_exists( $instance, static::INJECTION_METHOD ) ) {
|
||||
call_user_func_array( array( $instance, static::INJECTION_METHOD ), $resolved );
|
||||
}
|
||||
}
|
||||
|
||||
return $concrete;
|
||||
/**
|
||||
* Forget the cached resolved object, so the next time it's requested
|
||||
* it will be resolved again.
|
||||
*/
|
||||
public function forgetResolved() {
|
||||
$this->resolved = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement;
|
||||
|
||||
use Automattic\WooCommerce\Container;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\StringUtil;
|
||||
use Automattic\WooCommerce\Vendor\League\Container\Container as BaseContainer;
|
||||
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;
|
||||
@@ -23,6 +25,13 @@ class ExtendedContainer extends BaseContainer {
|
||||
*/
|
||||
private $woocommerce_namespace = 'Automattic\\WooCommerce\\';
|
||||
|
||||
/**
|
||||
* Holds the original registrations so that 'reset_replacement' can work, keys are class names and values are the original concretes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $original_concretes = array();
|
||||
|
||||
/**
|
||||
* Whitelist of classes that we can register using the container
|
||||
* despite not belonging to the WooCommerce root namespace.
|
||||
@@ -68,7 +77,7 @@ class ExtendedContainer extends BaseContainer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing registration with a different concrete.
|
||||
* Replace an existing registration with a different concrete. See also 'reset_replacement' and 'reset_all_replacements'.
|
||||
*
|
||||
* @param string $class_name The class name whose definition will be replaced.
|
||||
* @param mixed $concrete The new concrete (same as "add").
|
||||
@@ -86,17 +95,49 @@ class ExtendedContainer extends BaseContainer {
|
||||
throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( $class_name, $this->original_concretes ) ) {
|
||||
// LegacyProxy is a special case: we replace it with MockableLegacyProxy at unit testing bootstrap time.
|
||||
$original_concrete = LegacyProxy::class === $class_name ? MockableLegacyProxy::class : $this->extend( $class_name )->getConcrete( $concrete );
|
||||
$this->original_concretes[ $class_name ] = $original_concrete;
|
||||
}
|
||||
|
||||
return $this->extend( $class_name )->setConcrete( $concrete );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a replaced registration back to its original concrete.
|
||||
*
|
||||
* @param string $class_name The class name whose definition had been replaced.
|
||||
* @return bool True if the registration has been reset, false if no replacement had been made for the specified class name.
|
||||
*/
|
||||
public function reset_replacement( string $class_name ) : bool {
|
||||
if ( ! array_key_exists( $class_name, $this->original_concretes ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->extend( $class_name )->setConcrete( $this->original_concretes[ $class_name ] );
|
||||
unset( $this->original_concretes[ $class_name ] );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the replaced registrations back to their original concretes.
|
||||
*/
|
||||
public function reset_all_replacements() {
|
||||
foreach ( $this->original_concretes as $class_name => $concrete ) {
|
||||
$this->extend( $class_name )->setConcrete( $concrete );
|
||||
}
|
||||
|
||||
$this->original_concretes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again.
|
||||
*/
|
||||
public function reset_all_resolved() {
|
||||
foreach ( $this->definitions->getIterator() as $definition ) {
|
||||
// setConcrete causes the cached resolved value to be forgotten.
|
||||
$concrete = $definition->getConcrete();
|
||||
$definition->setConcrete( $concrete );
|
||||
$definition->forgetResolved();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* BlockTemplatesServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\TemplateTransformer;
|
||||
|
||||
/**
|
||||
* Service provider for the block templates controller classes in the Automattic\WooCommerce\Internal\BlockTemplateRegistry namespace.
|
||||
*/
|
||||
class BlockTemplatesServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
BlockTemplateRegistry::class,
|
||||
BlockTemplatesController::class,
|
||||
TemplateTransformer::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( TemplateTransformer::class );
|
||||
$this->share( BlockTemplateRegistry::class );
|
||||
$this->share( BlockTemplatesController::class )->addArguments(
|
||||
array(
|
||||
BlockTemplateRegistry::class,
|
||||
TemplateTransformer::class,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\Edit;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
|
||||
/**
|
||||
@@ -26,6 +29,8 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
|
||||
PageController::class,
|
||||
Edit::class,
|
||||
ListTable::class,
|
||||
EditLock::class,
|
||||
TaxonomiesMetaBox::class,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -38,5 +43,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
|
||||
$this->share( PageController::class );
|
||||
$this->share( Edit::class )->addArgument( PageController::class );
|
||||
$this->share( ListTable::class )->addArgument( PageController::class );
|
||||
$this->share( EditLock::class );
|
||||
$this->share( TaxonomiesMetaBox::class )->addArgument( OrdersTableDataStore::class );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStoreMeta;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
|
||||
/**
|
||||
* Service provider for the classes in the Internal\DataStores\Orders namespace.
|
||||
@@ -69,6 +70,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
|
||||
FeaturesController::class,
|
||||
OrderCache::class,
|
||||
OrderCacheController::class,
|
||||
PluginUtil::class,
|
||||
)
|
||||
);
|
||||
$this->share( OrderCache::class );
|
||||
|
||||
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider
|
||||
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
|
||||
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
@@ -31,6 +32,7 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
|
||||
OrderUtil::class,
|
||||
PluginUtil::class,
|
||||
COTMigrationUtil::class,
|
||||
WebhookUtil::class,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -44,5 +46,6 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
|
||||
->addArgument( LegacyProxy::class );
|
||||
$this->share( COTMigrationUtil::class )
|
||||
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
|
||||
$this->share( WebhookUtil::class );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Analytics;
|
||||
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
|
||||
use Automattic\WooCommerce\Admin\Features\NewProductManagementExperience;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
@@ -89,33 +91,43 @@ class FeaturesController {
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$features = array(
|
||||
'analytics' => array(
|
||||
$hpos_enable_sync = DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
|
||||
$hpos_authoritative = CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
|
||||
$features = array(
|
||||
'analytics' => array(
|
||||
'name' => __( 'Analytics', 'woocommerce' ),
|
||||
'description' => __( 'Enables WooCommerce Analytics', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'enabled_by_default' => true,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
'new_navigation' => array(
|
||||
'new_navigation' => array(
|
||||
'name' => __( 'Navigation', 'woocommerce' ),
|
||||
'description' => __( 'Adds the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
'new_product_management' => array(
|
||||
'product_block_editor' => array(
|
||||
'name' => __( 'New product editor', 'woocommerce' ),
|
||||
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
'custom_order_tables' => array(
|
||||
'name' => __( 'High-Performance order storage (COT)', 'woocommerce' ),
|
||||
'description' => __( 'Enable the high performance order storage feature.', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'disable_ui' => false,
|
||||
// Options for HPOS features are added in CustomOrdersTableController to keep the logic in same place.
|
||||
'custom_order_tables' => array( // This exists for back-compat only, otherwise it's value is superseded by $hpos_authoritative option.
|
||||
'name' => __( 'High-Performance order storage (COT)', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'enabled_by_default' => false,
|
||||
),
|
||||
'cart_checkout_blocks' => array(
|
||||
$hpos_authoritative => array(
|
||||
'name' => __( 'High performance order storage', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
),
|
||||
$hpos_enable_sync => array(
|
||||
'name' => '',
|
||||
'is_experimental' => true,
|
||||
),
|
||||
'cart_checkout_blocks' => array(
|
||||
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
|
||||
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
@@ -123,7 +135,14 @@ class FeaturesController {
|
||||
),
|
||||
);
|
||||
|
||||
$this->legacy_feature_ids = array( 'analytics', 'new_navigation', 'new_product_management' );
|
||||
$this->legacy_feature_ids = array(
|
||||
'analytics',
|
||||
'new_navigation',
|
||||
'product_block_editor',
|
||||
// Compatibility for COT is determined by `custom_order_tables'.
|
||||
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
);
|
||||
|
||||
$this->init_features( $features );
|
||||
|
||||
@@ -134,9 +153,12 @@ class FeaturesController {
|
||||
self::add_filter( 'deactivated_plugin', array( $this, 'handle_plugin_deactivation' ), 10, 1 );
|
||||
self::add_filter( 'all_plugins', array( $this, 'filter_plugins_list' ), 10, 1 );
|
||||
self::add_action( 'admin_notices', array( $this, 'display_notices_in_plugins_page' ), 10, 0 );
|
||||
self::add_action( 'load-plugins.php', array( $this, 'maybe_invalidate_cached_plugin_data' ) );
|
||||
self::add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 );
|
||||
self::add_action( 'current_screen', array( $this, 'enqueue_script_to_fix_plugin_list_html' ), 10, 1 );
|
||||
self::add_filter( 'views_plugins', array( $this, 'handle_plugins_page_views_list' ), 10, 1 );
|
||||
self::add_filter( 'woocommerce_admin_shared_settings', array( $this, 'set_change_feature_enable_nonce' ), 20, 1 );
|
||||
self::add_action( 'admin_init', array( $this, 'change_feature_enable_from_query_params' ), 20, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +242,8 @@ class FeaturesController {
|
||||
}
|
||||
|
||||
$default_value = $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no';
|
||||
return 'yes' === get_option( $this->feature_enable_option_name( $feature_id ), $default_value );
|
||||
$value = 'yes' === get_option( $this->feature_enable_option_name( $feature_id ), $default_value );
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,15 +428,19 @@ class FeaturesController {
|
||||
* @return string The option that enables or disables the feature.
|
||||
*/
|
||||
public function feature_enable_option_name( string $feature_id ): string {
|
||||
if ( 'analytics' === $feature_id ) {
|
||||
return Analytics::TOGGLE_OPTION_NAME;
|
||||
} elseif ( 'new_navigation' === $feature_id ) {
|
||||
return Init::TOGGLE_OPTION_NAME;
|
||||
} elseif ( 'new_product_management' === $feature_id ) {
|
||||
return NewProductManagementExperience::TOGGLE_OPTION_NAME;
|
||||
switch ( $feature_id ) {
|
||||
case 'analytics':
|
||||
return Analytics::TOGGLE_OPTION_NAME;
|
||||
case 'new_navigation':
|
||||
return Init::TOGGLE_OPTION_NAME;
|
||||
case 'custom_order_tables':
|
||||
case CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
|
||||
return CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
|
||||
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
|
||||
return DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
|
||||
default:
|
||||
return "woocommerce_feature_{$feature_id}_enabled";
|
||||
}
|
||||
|
||||
return "woocommerce_feature_{$feature_id}_enabled";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -468,7 +495,15 @@ class FeaturesController {
|
||||
$matches = array();
|
||||
$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
|
||||
|
||||
if ( ! $success && Analytics::TOGGLE_OPTION_NAME !== $option && Init::TOGGLE_OPTION_NAME !== $option && NewProductManagementExperience::TOGGLE_OPTION_NAME !== $option ) {
|
||||
$known_features = array(
|
||||
Analytics::TOGGLE_OPTION_NAME,
|
||||
Init::TOGGLE_OPTION_NAME,
|
||||
NewProductManagementExperience::TOGGLE_OPTION_NAME,
|
||||
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
);
|
||||
|
||||
if ( ! $success && ! in_array( $option, $known_features, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -480,8 +515,8 @@ class FeaturesController {
|
||||
$feature_id = 'analytics';
|
||||
} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
|
||||
$feature_id = 'new_navigation';
|
||||
} elseif ( NewProductManagementExperience::TOGGLE_OPTION_NAME === $option ) {
|
||||
$feature_id = 'new_product_management';
|
||||
} elseif ( in_array( $option, $known_features, true ) ) {
|
||||
$feature_id = $option;
|
||||
} else {
|
||||
$feature_id = $matches[1];
|
||||
}
|
||||
@@ -552,7 +587,7 @@ class FeaturesController {
|
||||
$experimental_feature_ids = array_filter(
|
||||
$feature_ids,
|
||||
function( $feature_id ) use ( $features ) {
|
||||
return $features[ $feature_id ]['is_experimental'];
|
||||
return $features[ $feature_id ]['is_experimental'] ?? false;
|
||||
}
|
||||
);
|
||||
$mature_feature_ids = array_diff( $feature_ids, $experimental_feature_ids );
|
||||
@@ -566,11 +601,9 @@ class FeaturesController {
|
||||
*
|
||||
* @param bool $disabled False.
|
||||
*/
|
||||
$additional_features = apply_filters( 'woocommerce_settings_features', $features );
|
||||
$feature_settings = apply_filters( 'woocommerce_settings_features', $feature_settings );
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
|
||||
$feature_settings = array_merge( $feature_settings, $additional_features );
|
||||
|
||||
if ( ! empty( $experimental_feature_ids ) ) {
|
||||
$feature_settings[] = array(
|
||||
'type' => 'sectionend',
|
||||
@@ -611,10 +644,11 @@ class FeaturesController {
|
||||
* @return array The parameters to add to the settings array.
|
||||
*/
|
||||
private function get_setting_for_feature( string $feature_id, array $feature, bool $admin_features_disabled ): array {
|
||||
$description = $feature['description'];
|
||||
$description = $feature['description'] ?? '';
|
||||
$disabled = false;
|
||||
$desc_tip = '';
|
||||
$tooltip = isset( $feature['tooltip'] ) ? $feature['tooltip'] : '';
|
||||
$tooltip = $feature['tooltip'] ?? '';
|
||||
$type = $feature['type'] ?? 'checkbox';
|
||||
|
||||
if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) {
|
||||
$disabled = true;
|
||||
@@ -657,56 +691,17 @@ class FeaturesController {
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'new_product_management' === $feature_id ) {
|
||||
$disabled = true;
|
||||
$desc_tip = __( '⚠ This feature will be available soon. Stay tuned!', 'woocommerce' );
|
||||
if ( 'product_block_editor' === $feature_id ) {
|
||||
$disabled = version_compare( get_bloginfo( 'version' ), '6.2', '<' );
|
||||
if ( $disabled ) {
|
||||
$desc_tip = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
|
||||
$disabled = ! $this->feature_is_enabled( $feature_id );
|
||||
$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
|
||||
$incompatibles = array_merge( $plugin_info_for_feature['incompatible'], $plugin_info_for_feature['uncertain'] );
|
||||
$incompatibles = array_filter( $incompatibles, 'is_plugin_active' );
|
||||
$incompatible_count = count( $incompatibles );
|
||||
if ( $incompatible_count > 0 ) {
|
||||
if ( 1 === $incompatible_count ) {
|
||||
/* translators: %s = printable plugin name */
|
||||
$desc_tip = sprintf( __( "⚠ This feature shouldn't be enabled, the %s plugin is active and isn't compatible with it.", 'woocommerce' ), $this->plugin_util->get_plugin_name( $incompatibles[0] ) );
|
||||
} elseif ( 2 === $incompatible_count ) {
|
||||
/* translators: %1\$s, %2\$s = printable plugin names */
|
||||
$desc_tip = sprintf(
|
||||
__( "⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it.", 'woocommerce' ),
|
||||
$this->plugin_util->get_plugin_name( $incompatibles[0] ),
|
||||
$this->plugin_util->get_plugin_name( $incompatibles[1] )
|
||||
);
|
||||
} else {
|
||||
/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
|
||||
$desc_tip = sprintf(
|
||||
_n(
|
||||
"⚠ This feature shouldn't be enabled: %1\$s, %2\$s and %3\$d more active plugin isn't compatible with it",
|
||||
"⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it. There are %3\$d other incompatible plugins.",
|
||||
$incompatible_count - 2,
|
||||
'woocommerce'
|
||||
),
|
||||
$this->plugin_util->get_plugin_name( $incompatibles[0] ),
|
||||
$this->plugin_util->get_plugin_name( $incompatibles[1] ),
|
||||
$incompatible_count - 2
|
||||
);
|
||||
}
|
||||
|
||||
$incompatible_plugins_url = add_query_arg(
|
||||
array(
|
||||
'plugin_status' => 'incompatible_with_feature',
|
||||
'feature_id' => $feature_id,
|
||||
),
|
||||
admin_url( 'plugins.php' )
|
||||
);
|
||||
/* translators: %s = URL of the plugins page */
|
||||
$extra_desc_tip = sprintf( __( " <a href='%s'>Manage incompatible plugins</a>", 'woocommerce' ), $incompatible_plugins_url );
|
||||
|
||||
$desc_tip .= $extra_desc_tip;
|
||||
|
||||
$disabled = ! $this->feature_is_enabled( $feature_id );
|
||||
}
|
||||
$desc_tip = $this->plugin_util->generate_incompatible_plugin_feature_warning( $feature_id, $plugin_info_for_feature );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -721,16 +716,33 @@ class FeaturesController {
|
||||
*/
|
||||
$desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled );
|
||||
|
||||
return array(
|
||||
$feature_setting = array(
|
||||
'title' => $feature['name'],
|
||||
'desc' => $description,
|
||||
'type' => 'checkbox',
|
||||
'type' => $type,
|
||||
'id' => $this->feature_enable_option_name( $feature_id ),
|
||||
'disabled' => $disabled && ! $this->force_allow_enabling_features,
|
||||
'desc_tip' => $desc_tip,
|
||||
'tooltip' => $tooltip,
|
||||
'default' => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
|
||||
);
|
||||
|
||||
/**
|
||||
* Allows to modify feature setting that will be used to render in the feature page.
|
||||
*
|
||||
* @param array $feature_setting The feature setting. Describes the feature:
|
||||
* - title: The title of the feature.
|
||||
* - desc: The description of the feature. Will be displayed under the title.
|
||||
* - type: The type of the feature. Could be any of supported settings types from `WC_Admin_Settings::output_fields`, but if it's anything other than checkbox or radio, it will need custom handling.
|
||||
* - id: The id of the feature. Will be used as the name of the setting.
|
||||
* - disabled: Whether the feature is disabled or not.
|
||||
* - desc_tip: The description tip of the feature. Will be displayed as a tooltip next to the description.
|
||||
* - tooltip: The tooltip of the feature. Will be displayed as a tooltip next to the name.
|
||||
* - default: The default value of the feature.
|
||||
* @param string $feature_id The id of the feature.
|
||||
* @since 8.0.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_feature_setting', $feature_setting, $feature_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -775,6 +787,18 @@ class FeaturesController {
|
||||
return $list;
|
||||
}
|
||||
|
||||
return $this->get_incompatible_plugins( $feature_id, $list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of plugins incompatible with a given feature.
|
||||
*
|
||||
* @param string $feature_id ID of the feature. Can also be `all` to denote all features.
|
||||
* @param array $list List of plugins to filter.
|
||||
*
|
||||
* @return array List of plugins incompatible with the given feature.
|
||||
*/
|
||||
private function get_incompatible_plugins( $feature_id, $list ) {
|
||||
$incompatibles = array();
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
|
||||
@@ -817,6 +841,10 @@ class FeaturesController {
|
||||
* there's already a "You are viewing
|
||||
*/
|
||||
private function maybe_display_feature_incompatibility_warning(): void {
|
||||
if ( ! current_user_can( 'activate_plugins' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$incompatible_plugins = false;
|
||||
|
||||
foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
|
||||
@@ -907,6 +935,24 @@ class FeaturesController {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the 'incompatible with features' plugin list is being rendered, invalidate existing cached plugin data.
|
||||
*
|
||||
* This heads off a problem in which WordPress's `get_plugins()` function may be called much earlier in the request
|
||||
* (by third party code, for example), the results of which are cached, and before WooCommerce can modify the list
|
||||
* to inject useful information of its own.
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce/issues/37343
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_invalidate_cached_plugin_data(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
if ( ( $_GET['plugin_status'] ?? '' ) === 'incompatible_with_feature' ) {
|
||||
wp_cache_delete( 'plugins', 'plugins' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the 'after_plugin_row' action.
|
||||
* Displays a "This plugin is incompatible with X features" notice if necessary.
|
||||
@@ -1084,4 +1130,54 @@ class FeaturesController {
|
||||
'incompatible_with_feature' => $incompatible_link,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feature nonce to be sent from client side.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function set_change_feature_enable_nonce( $settings ) {
|
||||
$settings['_feature_nonce'] = wp_create_nonce( 'change_feature_enable' );
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the feature given it's id, a toggle value and nonce as a query param.
|
||||
*
|
||||
* `/wp-admin/post.php?product_block_editor=1&_feature_nonce=1234`, 1 for on
|
||||
* `/wp-admin/post.php?product_block_editor=0&_feature_nonce=1234`, 0 for off
|
||||
*/
|
||||
private function change_feature_enable_from_query_params(): void {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$is_feature_nonce_invalid = ( ! isset( $_GET['_feature_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_feature_nonce'] ) ), 'change_feature_enable' ) );
|
||||
|
||||
$query_params_to_remove = array( '_feature_nonce' );
|
||||
|
||||
foreach ( array_keys( $this->features ) as $feature_id ) {
|
||||
if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) {
|
||||
$value = absint( $_GET[ $feature_id ] );
|
||||
|
||||
if ( $is_feature_nonce_invalid ) {
|
||||
wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 1 === $value ) {
|
||||
$this->change_feature_enable( $feature_id, true );
|
||||
} elseif ( 0 === $value ) {
|
||||
$this->change_feature_enable( $feature_id, false );
|
||||
}
|
||||
$query_params_to_remove[] = $feature_id;
|
||||
}
|
||||
}
|
||||
if ( count( $query_params_to_remove ) > 1 && isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
wp_safe_redirect( remove_query_arg( $query_params_to_remove, $_SERVER['REQUEST_URI'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ class OptionSanitizer {
|
||||
2
|
||||
);
|
||||
}
|
||||
// Cast "Out of stock threshold" field to absolute integer to prevent storing empty value.
|
||||
self::add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_notify_no_stock_amount', 'absint' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,8 +33,11 @@ class DatabaseUtil {
|
||||
* @return array An array containing the names of the tables that currently don't exist in the database.
|
||||
*/
|
||||
public function get_missing_tables( string $creation_queries ): array {
|
||||
$dbdelta_output = $this->dbdelta( $creation_queries, false );
|
||||
$parsed_output = $this->parse_dbdelta_output( $dbdelta_output );
|
||||
global $wpdb;
|
||||
$suppress_errors = $wpdb->suppress_errors( true );
|
||||
$dbdelta_output = $this->dbdelta( $creation_queries, false );
|
||||
$wpdb->suppress_errors( $suppress_errors );
|
||||
$parsed_output = $this->parse_dbdelta_output( $dbdelta_output );
|
||||
return $parsed_output['created_tables'];
|
||||
}
|
||||
|
||||
@@ -126,15 +129,14 @@ class DatabaseUtil {
|
||||
$index_name = 'PRIMARY';
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL
|
||||
return $wpdb->get_col(
|
||||
"
|
||||
SELECT column_name FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_name='$table_name'
|
||||
AND table_schema='" . DB_NAME . "'
|
||||
AND index_name='$index_name'"
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$results = $wpdb->get_results( $wpdb->prepare( "SHOW INDEX FROM $table_name WHERE Key_name = %s", $index_name ) );
|
||||
|
||||
if ( empty( $results ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_column( $results, 'Column_name' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,23 +233,62 @@ AND index_name='$index_name'"
|
||||
*/
|
||||
public function insert_on_duplicate_key_update( $table_name, $data, $format ) : int {
|
||||
global $wpdb;
|
||||
if ( empty( $data ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$columns = array_keys( $data );
|
||||
$columns = array_keys( $data );
|
||||
$value_format = array();
|
||||
$values = array();
|
||||
$index = 0;
|
||||
// Directly use NULL for placeholder if the value is NULL, since otherwise $wpdb->prepare will convert it to empty string.
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( is_null( $value ) ) {
|
||||
$value_format[] = 'NULL';
|
||||
} else {
|
||||
$values[] = $value;
|
||||
$value_format[] = $format[ $index ];
|
||||
}
|
||||
$index++;
|
||||
}
|
||||
$column_clause = '`' . implode( '`, `', $columns ) . '`';
|
||||
$value_placeholders = implode( ', ', array_values( $format ) );
|
||||
$value_format_clause = implode( ', ', $value_format );
|
||||
$on_duplicate_clause = $this->generate_on_duplicate_statement_clause( $columns );
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Values are escaped in $wpdb->prepare.
|
||||
$sql = $wpdb->prepare(
|
||||
"
|
||||
INSERT INTO $table_name ( $column_clause )
|
||||
VALUES ( $value_placeholders )
|
||||
VALUES ( $value_format_clause )
|
||||
$on_duplicate_clause
|
||||
",
|
||||
array_values( $data )
|
||||
$values
|
||||
);
|
||||
// phpcs:enable
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is prepared.
|
||||
return $wpdb->query( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max index length.
|
||||
*
|
||||
* @return int Max index length.
|
||||
*/
|
||||
public function get_max_index_length() : int {
|
||||
/**
|
||||
* Filters the maximum index length in the database.
|
||||
*
|
||||
* Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
|
||||
* As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which
|
||||
* used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
|
||||
*
|
||||
* Additionally, MyISAM engine also limits the index size to 1000 bytes. We add this filter so that interested folks on InnoDB engine can increase the size till allowed 3071 bytes.
|
||||
*
|
||||
* @param int $max_index_length Maximum index length. Default 191.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
$max_index_length = apply_filters( 'woocommerce_database_max_index_length', 191 );
|
||||
// Index length cannot be more than 768, which is 3078 bytes in utf8mb4 and max allowed by InnoDB engine.
|
||||
return min( absint( $max_index_length ), 767 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* WebhookUtil class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Utilities;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Class with utility methods for dealing with webhooks.
|
||||
*/
|
||||
class WebhookUtil {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_action( 'deleted_user', array( $this, 'reassign_webhooks_to_new_user_id' ), 10, 2 );
|
||||
self::add_action( 'delete_user_form', array( $this, 'maybe_render_user_with_webhooks_warning' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever a user is deleted, re-assign their webhooks to the new user.
|
||||
*
|
||||
* If re-assignment isn't selected during deletion, assign the webhooks to user_id 0,
|
||||
* so that an admin can edit and re-save them in order to get them to be assigned to a valid user.
|
||||
*
|
||||
* @param int $old_user_id ID of the deleted user.
|
||||
* @param int|null $new_user_id ID of the user to reassign existing data to, or null if no re-assignment is requested.
|
||||
*
|
||||
* @return void
|
||||
* @since 7.8.0
|
||||
*/
|
||||
private function reassign_webhooks_to_new_user_id( int $old_user_id, ?int $new_user_id ): void {
|
||||
$webhook_ids = $this->get_webhook_ids_for_user( $old_user_id );
|
||||
|
||||
foreach ( $webhook_ids as $webhook_id ) {
|
||||
$webhook = new \WC_Webhook( $webhook_id );
|
||||
$webhook->set_user_id( $new_user_id ?? 0 );
|
||||
$webhook->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When users are about to be deleted show an informative text if they have webhooks assigned.
|
||||
*
|
||||
* @param \WP_User $current_user The current logged in user.
|
||||
* @param array $userids Array with the ids of the users that are about to be deleted.
|
||||
* @return void
|
||||
* @since 7.8.0
|
||||
*/
|
||||
private function maybe_render_user_with_webhooks_warning( \WP_User $current_user, array $userids ): void {
|
||||
global $wpdb;
|
||||
|
||||
$at_least_one_user_with_webhooks = false;
|
||||
|
||||
foreach ( $userids as $user_id ) {
|
||||
$webhook_ids = $this->get_webhook_ids_for_user( $user_id );
|
||||
if ( empty( $webhook_ids ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$at_least_one_user_with_webhooks = true;
|
||||
|
||||
$user_data = get_userdata( $user_id );
|
||||
$user_login = false === $user_data ? '' : $user_data->user_login;
|
||||
$webhooks_count = count( $webhook_ids );
|
||||
|
||||
$text = sprintf(
|
||||
/* translators: 1 = user id, 2 = user login, 3 = webhooks count */
|
||||
_nx(
|
||||
'User #%1$s %2$s has created %3$d WooCommerce webhook.',
|
||||
'User #%1$s %2$s has created %3$d WooCommerce webhooks.',
|
||||
$webhooks_count,
|
||||
'user webhook count',
|
||||
'woocommerce'
|
||||
),
|
||||
$user_id,
|
||||
$user_login,
|
||||
$webhooks_count
|
||||
);
|
||||
|
||||
echo '<p>' . esc_html( $text ) . '</p>';
|
||||
}
|
||||
|
||||
if ( ! $at_least_one_user_with_webhooks ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$webhooks_settings_url = esc_url_raw( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) );
|
||||
|
||||
// This block of code is copied from WordPress' users.php.
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared
|
||||
$users_have_content = (bool) apply_filters( 'users_have_additional_content', false, $userids );
|
||||
if ( ! $users_have_content ) {
|
||||
if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
|
||||
$users_have_content = true;
|
||||
} elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
|
||||
$users_have_content = true;
|
||||
}
|
||||
}
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
if ( $users_have_content ) {
|
||||
$text = __( 'If the "Delete all content" option is selected, the affected WooCommerce webhooks will <b>not</b> be deleted and will be attributed to user id 0.<br/>', 'woocommerce' );
|
||||
} else {
|
||||
$text = __( 'The affected WooCommerce webhooks will <b>not</b> be deleted and will be attributed to user id 0.<br/>', 'woocommerce' );
|
||||
}
|
||||
|
||||
$text .= sprintf(
|
||||
/* translators: 1 = url of the WooCommerce webhooks settings page */
|
||||
__( 'After that they can be reassigned to the logged-in user by going to the <a href="%1$s">WooCommerce webhooks settings page</a> and re-saving them.', 'woocommerce' ),
|
||||
$webhooks_settings_url
|
||||
);
|
||||
|
||||
echo '<p>' . wp_kses_post( $text ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of the webhooks assigned to a given user.
|
||||
*
|
||||
* @param int $user_id User id.
|
||||
* @return int[] Array of webhook ids.
|
||||
*/
|
||||
private function get_webhook_ids_for_user( int $user_id ): array {
|
||||
$data_store = \WC_Data_Store::load( 'webhook' );
|
||||
return $data_store->search_webhooks(
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user