Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -0,0 +1,78 @@
<?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.
*
* @var array
*/
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 BlockTemplateInterface $template Template to register.
*
* @throws \ValueError If a template with the same ID already exists.
*/
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.' );
}
/**
* Fires when a template is registered.
*
* @param BlockTemplateInterface $template Template that was registered.
*
* @since 8.2.0
*/
do_action( 'woocommerce_block_template_register', $template );
$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;
}
}

View File

@@ -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 );
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,253 @@
<?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 {
use BlockFormattedTemplateTrait;
/**
* 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 hide conditions.
*
* @var array
*/
private $hide_conditions = [];
/**
* The block hide conditions counter.
*
* @var int
*/
private $hide_conditions_counter = 0;
/**
* 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 ];
}
if ( isset( $config[ self::HIDE_CONDITIONS_KEY ] ) ) {
foreach ( $config[ self::HIDE_CONDITIONS_KEY ] as $hide_condition ) {
$this->add_hide_condition( $hide_condition['expression'] );
}
}
}
/**
* 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;
}
/**
* Remove the block from its parent.
*/
public function remove() {
$this->parent->remove_block( $this->id );
}
/**
* Check if the block is detached from its parent block container or the template it belongs to.
*
* @return bool True if the block is detached from its parent block container or the template it belongs to.
*/
public function is_detached(): bool {
$is_in_parent = $this->parent->get_block( $this->id ) === $this;
$is_in_root_template = $this->get_root_template()->get_block( $this->id ) === $this;
return ! ( $is_in_parent && $is_in_root_template );
}
/**
* Add a hide condition to the block.
*
* The hide condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden.
* See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details.
*
* @param string $expression An expression, which if true, will hide the block.
*/
public function add_hide_condition( string $expression ): string {
$key = 'k' . $this->hide_conditions_counter;
$this->hide_conditions_counter++;
// Storing the expression in an array to allow for future expansion
// (such as adding the plugin that added the condition).
$this->hide_conditions[ $key ] = [
'expression' => $expression,
];
return $key;
}
/**
* Remove a hide condition from the block.
*
* @param string $key The key of the hide condition to remove.
*/
public function remove_hide_condition( string $key ) {
unset( $this->hide_conditions[ $key ] );
}
/**
* Get the hide conditions of the block.
*/
public function get_hide_conditions(): array {
return $this->hide_conditions;
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
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.
*/
abstract public 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 from the BlockContainerTrait's add_inner_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;
}
/**
* Uncaches a block in the template. This is an internal method and should not be called directly
* except for from the BlockContainerTrait's remove_block() method.
*
* @param string $block_id The block ID.
*
* @ignore
*/
public function uncache_block( string $block_id ) {
if ( isset( $this->block_cache[ $block_id ] ) ) {
unset( $this->block_cache[ $block_id ] );
}
}
/**
* 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( BlockInterface $block ) {
return $block->get_formatted_template();
},
$inner_blocks
);
return $inner_blocks_formatted_template;
}
}

View File

@@ -0,0 +1,24 @@
<?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;
/**
* 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 );
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
/**
* Trait for block containers.
*/
trait BlockContainerTrait {
use BlockFormattedTemplateTrait {
get_formatted_template as get_block_formatted_template;
}
/**
* 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.
* @throws \UnexpectedValueException If the block container's root template is not the same as the block's root template.
*/
protected function &add_inner_block( BlockInterface $block ): BlockInterface {
if ( $block->get_parent() !== $this ) {
throw new \UnexpectedValueException( 'The block container is not the parent of the block.' );
}
if ( $block->get_root_template() !== $this->get_root_template() ) {
throw new \UnexpectedValueException( 'The block container\'s root template is not the same as the block\'s root template.' );
}
$is_detached = method_exists( $this, 'is_detached' ) && $this->is_detached();
if ( $is_detached ) {
BlockTemplateLogger::get_instance()->warning(
'Block added to detached container. Block will not be included in the template, since the container will not be included in the template.',
[
'block' => $block,
'container' => $this,
'template' => $this->get_root_template(),
]
);
} else {
$this->get_root_template()->cache_block( $block );
}
$this->inner_blocks[] = &$block;
$this->do_after_add_block_action( $block );
$this->do_after_add_specific_block_action( $block );
return $block;
}
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Checks if a block is a descendant of the block container.
*
* @param BlockInterface $block The block.
*/
private function is_block_descendant( BlockInterface $block ): bool {
$parent = $block->get_parent();
if ( $parent === $this ) {
return true;
}
if ( ! $parent instanceof BlockInterface ) {
return false;
}
return $this->is_block_descendant( $parent );
}
/**
* Get a block by ID.
*
* @param string $block_id The block ID.
*/
public function get_block( string $block_id ): ?BlockInterface {
foreach ( $this->inner_blocks as $block ) {
if ( $block->get_id() === $block_id ) {
return $block;
}
}
foreach ( $this->inner_blocks as $block ) {
if ( $block instanceof ContainerInterface ) {
$block = $block->get_block( $block_id );
if ( $block ) {
return $block;
}
}
}
return null;
}
/**
* Remove a block from the block container.
*
* @param string $block_id The block ID.
*
* @throws \UnexpectedValueException If the block container is not an ancestor of the block.
*/
public function remove_block( string $block_id ) {
$root_template = $this->get_root_template();
$block = $root_template->get_block( $block_id );
if ( ! $block ) {
return;
}
if ( ! $this->is_block_descendant( $block ) ) {
throw new \UnexpectedValueException( 'The block container is not an ancestor of the block.' );
}
// If the block is a container, remove all of its blocks.
if ( $block instanceof ContainerInterface ) {
$block->remove_blocks();
}
$parent = $block->get_parent();
$parent->remove_inner_block( $block );
}
/**
* Remove all blocks from the block container.
*/
public function remove_blocks() {
array_map(
function ( BlockInterface $block ) {
$this->remove_block( $block->get_id() );
},
$this->inner_blocks
);
}
/**
* Remove a block from the block container's inner blocks. This is an internal method and should not be called directly
* except for from the BlockContainerTrait's remove_block() method.
*
* @param BlockInterface $block The block.
*/
public function remove_inner_block( BlockInterface $block ) {
// Remove block from root template's cache.
$root_template = $this->get_root_template();
$root_template->uncache_block( $block->get_id() );
$this->inner_blocks = array_filter(
$this->inner_blocks,
function ( BlockInterface $inner_block ) use ( $block ) {
return $inner_block !== $block;
}
);
BlockTemplateLogger::get_instance()->info(
'Block removed from template.',
[
'block' => $block,
'template' => $root_template,
]
);
$this->do_after_remove_block_action( $block );
$this->do_after_remove_specific_block_action( $block );
}
/**
* 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( BlockInterface $a, BlockInterface $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_block_formatted_template();
$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;
}
/**
* Do the `woocommerce_block_template_after_add_block` action.
* Handle exceptions thrown by the action.
*
* @param BlockInterface $block The block.
*/
private function do_after_add_block_action( BlockInterface $block ) {
try {
/**
* Action called after a block is added to a block container.
*
* This action can be used to perform actions after a block is added to the block container,
* such as adding a dependent block.
*
* @param BlockInterface $block The block.
*
* @since 8.2.0
*/
do_action( 'woocommerce_block_template_after_add_block', $block );
} catch ( \Exception $e ) {
$this->handle_exception_doing_action(
'Error after adding block to template.',
'woocommerce_block_template_after_add_block',
$block,
$e
);
}
}
/**
* Do the `woocommerce_block_template_area_{template_area}_after_add_block_{block_id}` action.
* Handle exceptions thrown by the action.
*
* @param BlockInterface $block The block.
*/
private function do_after_add_specific_block_action( BlockInterface $block ) {
try {
/**
* Action called after a specific block is added to a template with a specific area.
*
* This action can be used to perform actions after a specific block is added to a template with a specific area,
* such as adding a dependent block.
*
* @param BlockInterface $block The block.
*
* @since 8.2.0
*/
do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}", $block );
} catch ( \Exception $e ) {
$this->handle_exception_doing_action(
'Error after adding block to template.',
"woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}",
$block,
$e
);
}
}
/**
* Do the `woocommerce_block_template_after_remove_block` action.
* Handle exceptions thrown by the action.
*
* @param BlockInterface $block The block.
*/
private function do_after_remove_block_action( BlockInterface $block ) {
try {
/**
* Action called after a block is removed from a block container.
*
* This action can be used to perform actions after a block is removed from the block container,
* such as removing a dependent block.
*
* @param BlockInterface $block The block.
*
* @since 8.2.0
*/
do_action( 'woocommerce_block_template_after_remove_block', $block );
} catch ( \Exception $e ) {
$this->handle_exception_doing_action(
'Error after removing block from template.',
'woocommerce_block_template_after_remove_block',
$block,
$e
);
}
}
/**
* Do the `woocommerce_block_template_area_{template_area}_after_remove_block_{block_id}` action.
* Handle exceptions thrown by the action.
*
* @param BlockInterface $block The block.
*/
private function do_after_remove_specific_block_action( BlockInterface $block ) {
try {
/**
* Action called after a specific block is removed from a template with a specific area.
*
* This action can be used to perform actions after a specific block is removed from a template with a specific area,
* such as removing a dependent block.
*
* @param BlockInterface $block The block.
*
* @since 8.2.0
*/
do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}", $block );
} catch ( \Exception $e ) {
$this->handle_exception_doing_action(
'Error after removing block from template.',
"woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}",
$block,
$e
);
}
}
/**
* Handle an exception thrown by an action.
*
* @param string $message The message.
* @param string $action_tag The action tag.
* @param BlockInterface $block The block.
* @param \Exception $e The exception.
*/
private function handle_exception_doing_action( string $message, string $action_tag, BlockInterface $block, \Exception $e ) {
BlockTemplateLogger::get_instance()->error(
$message,
[
'exception' => $e,
'action' => $action_tag,
'container' => $this,
'block' => $block,
'template' => $this->get_root_template(),
],
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
/**
* Trait for block formatted template.
*/
trait BlockFormattedTemplateTrait {
/**
* 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(),
array_merge(
$this->get_attributes(),
[
'_templateBlockId' => $this->get_id(),
'_templateBlockOrder' => $this->get_order(),
],
! empty( $this->get_hide_conditions() ) ? [
'_templateBlockHideConditions' => $this->get_formatted_hide_conditions(),
] : []
),
];
return $arr;
}
/**
* Get the block hide conditions formatted for inclusion in a formatted template.
*/
private function get_formatted_hide_conditions(): array {
$formatted_hide_conditions = array_map(
function( $hide_condition ) {
return [
'expression' => $hide_condition['expression'],
];
},
array_values( $this->get_hide_conditions() )
);
return $formatted_hide_conditions;
}
}

View File

@@ -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';
}
/**
* Add an inner block to this template.
*
* @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 );
}
}

View File

@@ -0,0 +1,180 @@
<?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;
/**
* Logger for block template modifications.
*/
class BlockTemplateLogger {
/**
* Singleton instance.
*
* @var BlockTemplateLogger
*/
protected static $instance = null;
/**
* Logger instance.
*
* @var \WC_Logger
*/
protected $logger = null;
/**
* Get the singleton instance.
*/
public static function get_instance(): BlockTemplateLogger {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
protected function __construct() {
$this->logger = wc_get_logger();
}
/**
* Log an informational message.
*
* @param string $message Message to log.
* @param array $info Additional info to log.
*/
public function info( string $message, array $info = [] ) {
$this->logger->info(
$this->format_message( $message, $info ),
[ 'source' => 'block_template' ]
);
}
/**
* Log a warning message.
*
* @param string $message Message to log.
* @param array $info Additional info to log.
*/
public function warning( string $message, array $info = [] ) {
$this->logger->warning(
$this->format_message( $message, $info ),
[ 'source' => 'block_template' ]
);
}
/**
* Log an error message.
*
* @param string $message Message to log.
* @param array $info Additional info to log.
*/
public function error( string $message, array $info = [] ) {
$this->logger->error(
$this->format_message( $message, $info ),
[ 'source' => 'block_template' ]
);
}
/**
* Format a message for logging.
*
* @param string $message Message to log.
* @param array $info Additional info to log.
*/
private function format_message( string $message, array $info = [] ): string {
$formatted_message = sprintf(
"%s\n%s",
$message,
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
print_r( $this->format_info( $info ), true ),
);
return $formatted_message;
}
/**
* Format info for logging.
*
* @param array $info Info to log.
*/
private function format_info( array $info ): array {
$formatted_info = $info;
if ( isset( $info['exception'] ) && $info['exception'] instanceof \Exception ) {
$formatted_info['exception'] = $this->format_exception( $info['exception'] );
}
if ( isset( $info['container'] ) ) {
if ( $info['container'] instanceof BlockContainerInterface ) {
$formatted_info['container'] = $this->format_block( $info['container'] );
} elseif ( $info['container'] instanceof BlockTemplateInterface ) {
$formatted_info['container'] = $this->format_template( $info['container'] );
} elseif ( $info['container'] instanceof BlockInterface ) {
$formatted_info['container'] = $this->format_block( $info['container'] );
}
}
if ( isset( $info['block'] ) && $info['block'] instanceof BlockInterface ) {
$formatted_info['block'] = $this->format_block( $info['block'] );
}
if ( isset( $info['template'] ) && $info['template'] instanceof BlockTemplateInterface ) {
$formatted_info['template'] = $this->format_template( $info['template'] );
}
return $formatted_info;
}
/**
* Format an exception for logging.
*
* @param \Exception $exception Exception to format.
*/
private function format_exception( \Exception $exception ): array {
return [
'message' => $exception->getMessage(),
'source' => "{$exception->getFile()}: {$exception->getLine()}",
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
'trace' => print_r( $this->format_exception_trace( $exception->getTrace() ), true ),
];
}
/**
* Format an exception trace for logging.
*
* @param array $trace Exception trace to format.
*/
private function format_exception_trace( array $trace ): array {
$formatted_trace = [];
foreach ( $trace as $source ) {
$formatted_trace[] = "{$source['file']}: {$source['line']}";
}
return $formatted_trace;
}
/**
* Format a block template for logging.
*
* @param BlockTemplateInterface $template Template to format.
*/
private function format_template( BlockTemplateInterface $template ): string {
return "{$template->get_id()} (area: {$template->get_area()})";
}
/**
* Format a block for logging.
*
* @param BlockInterface $block Block to format.
*/
private function format_block( BlockInterface $block ): string {
return "{$block->get_id()} (name: {$block->get_name()})";
}
}

View File

@@ -36,7 +36,6 @@ use Automattic\WooCommerce\Internal\Admin\Notes\PerformanceOnMobile;
use Automattic\WooCommerce\Internal\Admin\Notes\PersonalizeStore;
use Automattic\WooCommerce\Internal\Admin\Notes\RealTimeOrderAlerts;
use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
use Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
use Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles;
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
@@ -92,7 +91,6 @@ class Events {
PerformanceOnMobile::class,
PersonalizeStore::class,
RealTimeOrderAlerts::class,
TestCheckout::class,
TrackingOptIn::class,
WooCommercePayments::class,
WooCommerceSubscriptions::class,
@@ -156,7 +154,7 @@ class Events {
MerchantEmailNotifications::run();
}
if ( Features::is_enabled( 'onboarding' ) ) {
if ( Features::is_enabled( 'core-profiler' ) ) {
( new MailchimpScheduler() )->run();
}
}
@@ -180,7 +178,14 @@ class Events {
$note = clone $note_from_db;
$note->set_title( $note_from_class->get_title() );
$note->set_content( $note_from_class->get_content() );
$note->set_actions( $note_from_class->get_actions() );
$actions = $note_from_class->get_actions();
foreach ( $actions as $action ) {
$matching_action = $note->get_action( $action->name );
if ( $matching_action && $matching_action->id ) {
$action->id = $matching_action->id;
}
}
$note->set_actions( $actions );
return $note;
}
break;

View File

@@ -14,7 +14,6 @@ use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
use Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
@@ -177,7 +176,6 @@ class FeaturePlugin {
new TrackingOptIn();
new WooCommercePayments();
new InstallJPAndWCSPlugins();
new TestCheckout();
new SellingOnlineCourses();
new MagentoMigration();

View File

@@ -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' ) );
@@ -91,6 +93,8 @@ class Loader {
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
add_action( 'admin_init', array( __CLASS__, 'deactivate_wc_admin_plugin' ) );
add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) );
}
/**
@@ -173,7 +177,7 @@ class Loader {
}
$classes = explode( ' ', trim( $admin_body_class ) );
$classes[] = 'woocommerce-page';
$classes[] = 'woocommerce-admin-page';
if ( PageController::is_embed_page() ) {
$classes[] = 'woocommerce-embed-page';
}
@@ -332,9 +336,7 @@ class Loader {
}
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
if ( class_exists( 'Jetpack' ) ) {
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
}
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
if ( ! empty( $preload_data_endpoints ) ) {
$preload_data = array_reduce(
array_values( $preload_data_endpoints ),
@@ -569,4 +571,11 @@ class Loader {
delete_option( 'woocommerce_onboarding_homepage_post_id' );
}
}
/**
* Adds the appearance_theme_view Tracks event.
*/
public static function add_appearance_theme_view_tracks_event() {
wc_admin_record_tracks_event( 'appearance_theme_view', array() );
}
}

View File

@@ -136,36 +136,27 @@ class MarketingSpecs {
/**
* Load knowledge base posts from WooCommerce.com
*
* @param string|null $category Category of posts to retrieve.
* @param string|null $topic The topic of marketing knowledgebase 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,
);
// 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'];
public function get_knowledge_base_posts( ?string $topic ): array {
// Default to the marketing topic (if no topic is set on the kb component).
if ( empty( $topic ) ) {
$topic = 'marketing';
}
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT . '_' . strtolower( $topic );
$posts = get_transient( $kb_transient );
if ( false === $posts ) {
$request_url = add_query_arg(
array(
'categories' => $category_id,
'page' => 1,
'per_page' => 8,
'_embed' => 1,
'page' => 1,
'per_page' => 8,
'_embed' => 1,
),
'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product'
'https://woocommerce.com/wp-json/wccom/marketing-knowledgebase/v1/posts/' . $topic
);
$request = wp_remote_get(

View File

@@ -0,0 +1,56 @@
<?php
/**
* WooCommerce Marketplace.
*/
namespace Automattic\WooCommerce\Internal\Admin;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
/**
* Contains backend logic for the Marketplace feature.
*/
class Marketplace {
/**
* Class initialization, to be executed when the class is resolved by the container.
*/
final public function init() {
if ( FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
}
}
/**
* Registers report pages.
*/
public function register_pages() {
$marketplace_pages = self::get_marketplace_pages();
foreach ( $marketplace_pages as $marketplace_page ) {
if ( ! is_null( $marketplace_page ) ) {
wc_admin_register_page( $marketplace_page );
}
}
}
/**
* Get report pages.
*/
public static function get_marketplace_pages() {
$marketplace_pages = array(
array(
'id' => 'woocommerce-marketplace',
'parent' => 'woocommerce',
'title' => __( 'Extensions', 'woocommerce' ),
'path' => '/extensions',
),
);
/**
* The marketplace items used in the menu.
*
* @since 8.0
*/
return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages );
}
}

View File

@@ -1,104 +0,0 @@
<?php
/**
* WooCommerce Admin Test Checkout.
*
* Adds a note to remind the user to test their store checkout.
*
* @package WooCommerce\Admin
*/
namespace Automattic\WooCommerce\Internal\Admin\Notes;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
/**
* Test_Checkout
*/
class TestCheckout {
/**
* Note traits.
*/
use NoteTraits;
/**
* Name of the note for use in the database.
*/
const NOTE_NAME = 'wc-admin-test-checkout';
/**
* Completed tasks option name.
*/
const TASK_LIST_TRACKED_TASKS = 'woocommerce_task_list_tracked_completed_tasks';
/**
* Constructor.
*/
public function __construct() {
add_action( 'update_option_' . self::TASK_LIST_TRACKED_TASKS, array( $this, 'possibly_add_note' ) );
}
/**
* Get the note.
*
* @return Note|null
*/
public static function get_note() {
$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );
// Confirm that $onboarding_profile is set.
if ( empty( $onboarding_profile ) ) {
return;
}
// Make sure that the person who filled out the OBW was not setting up
// the store for their customer/client.
if (
! isset( $onboarding_profile['setup_client'] ) ||
$onboarding_profile['setup_client']
) {
return;
}
// Make sure payments task was completed.
$completed_tasks = get_option( self::TASK_LIST_TRACKED_TASKS, array() );
if ( ! in_array( 'payments', $completed_tasks, true ) ) {
return;
}
// Make sure that products were added within the previous 1/2 hour.
$query = new \WC_Product_Query(
array(
'limit' => 1,
'status' => 'publish',
'orderby' => 'date',
'order' => 'ASC',
)
);
$products = $query->get_products();
if ( 0 === count( $products ) ) {
return;
}
$oldest_product_timestamp = $products[0]->get_date_created()->getTimestamp();
$half_hour_in_seconds = 30 * MINUTE_IN_SECONDS;
if ( ( time() - $oldest_product_timestamp ) > $half_hour_in_seconds ) {
return;
}
$content = __( 'Make sure that your checkout is working properly before you launch your store. Go through your checkout process in its entirety: from adding a product to your cart, choosing a shipping location, and making a payment.', 'woocommerce' );
$note = new Note();
$note->set_title( __( 'Don\'t forget to test your checkout', 'woocommerce' ) );
$note->set_content( $content );
$note->set_content_data( (object) array() );
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'test-checkout', __( 'Test checkout', 'woocommerce' ), wc_get_page_permalink( 'shop' ) );
return $note;
}
}

View File

@@ -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';

View File

@@ -208,7 +208,6 @@ class Edit {
* @return void
*/
public function handle_order_update() {
global $theorder;
if ( ! isset( $this->order ) ) {
return;
}
@@ -233,6 +232,8 @@ class Edit {
*/
do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order );
$this->custom_meta_box->handle_metadata_changes($this->order);
// Order updated message.
$this->message = 1;
@@ -388,6 +389,7 @@ class Edit {
<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'; ?>">

View File

@@ -99,8 +99,10 @@ class EditLock {
return $response;
}
unset( $response['wp-refresh-post-lock'] );
$order = wc_get_order( $order_id );
if ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) {
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;
}

View File

@@ -230,6 +230,17 @@ class ListTable extends WP_List_Table {
$title = esc_html( $post_type->labels->name );
$add_new = esc_html( $post_type->labels->add_new );
$new_page_link = $this->page_controller->get_new_page_url( $this->order_type );
$search_label = '';
if ( ! empty( $this->order_query_args['s'] ) ) {
$search_label = '<span class="subtitle">';
$search_label .= sprintf(
/* translators: %s: Search query. */
__( 'Search results for: %s', 'woocommerce' ),
'<strong>' . esc_html( $this->order_query_args['s'] ) . '</strong>'
);
$search_label .= '</span>';
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wp_kses_post(
@@ -237,6 +248,7 @@ class ListTable extends WP_List_Table {
<div class='wrap'>
<h1 class='wp-heading-inline'>{$title}</h1>
<a href='" . esc_url( $new_page_link ) . "' class='page-title-action'>{$add_new}</a>
{$search_label}
<hr class='wp-header-end'>"
);
@@ -711,20 +723,15 @@ class ListTable extends WP_List_Table {
global $wpdb;
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
$utc_offset = wc_timezone_offset();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$order_dates = $wpdb->get_results(
"
SELECT DISTINCT YEAR( date_created_gmt ) AS year,
MONTH( date_created_gmt ) AS month
FROM $orders_table
WHERE status NOT IN (
'trash'
)
ORDER BY year DESC, month DESC;
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
MONTH( t.date_created_local ) AS month
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE status != 'trash' ) t
ORDER BY year DESC, month DESC
"
);
@@ -867,7 +874,7 @@ class ListTable extends WP_List_Table {
public function column_cb( $item ) {
ob_start();
?>
<input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="<?php echo esc_attr( $this->_args['singular'] ); ?>[]" value="<?php echo esc_attr( $item->get_id() ); ?>" />
<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>
@@ -1197,7 +1204,7 @@ class ListTable extends WP_List_Table {
$action = 'delete';
} else {
$ids = isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['order'] ) ) : array();
$ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array();
}
/**
@@ -1340,13 +1347,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' ) ) {

View File

@@ -242,21 +242,22 @@ class CustomMetaBox {
* @return void
*/
private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) {
$order_data_store = WC_Data_Store::load( 'order' );
$count = 0;
$count = 0;
if ( is_protected_meta( $meta_key ) ) {
wp_send_json_error( 'protected_meta' );
wp_die();
}
$meta_id = $order_data_store->add_meta(
$order,
new WC_Meta_Data(
array(
'key' => $meta_key,
'value' => $meta_value,
)
)
);
$metas_for_current_key = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) );
$meta_ids = wp_list_pluck( $metas_for_current_key, 'id' );
$order->add_meta_data( $meta_key, $meta_value );
$order->save_meta_data();
$metas_for_current_key_with_new = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) );
$meta_id = 0;
$new_meta_ids = wp_list_pluck( $metas_for_current_key_with_new, 'id' );
$new_meta_ids = array_values( array_diff( $new_meta_ids, $meta_ids ) );
if ( count( $new_meta_ids ) > 0 ) {
$meta_id = $new_meta_ids[0];
}
$response = new WP_Ajax_Response(
array(
'what' => 'meta',
@@ -305,16 +306,9 @@ class CustomMetaBox {
wp_die();
}
$order_data_store = WC_Data_Store::load( 'order' );
$count = 0;
$meta_object = new WC_Meta_Data(
array(
'id' => $mid,
'key' => $key,
'value' => $value,
)
);
$order_data_store->update_meta( $order, $meta_object );
$count = 0;
$order->update_meta_data( $key, $value, $mid );
$order->save_meta_data();
$response = new WP_Ajax_Response(
array(
'what' => 'meta',
@@ -415,4 +409,52 @@ class CustomMetaBox {
}
wp_die( 0 );
}
/**
* Handle the possible changes in order metadata coming from an order edit page in admin
* (labeled "custom fields" in the UI).
*
* This method expects the $_POST array to contain a 'meta' key that is an associative
* array of [meta item id => [ 'key' => meta item name, 'value' => meta item value ];
* and also to contain (possibly empty) 'metakeyinput' and 'metavalue' keys.
*
* @param WC_Order $order The order to handle.
*/
public function handle_metadata_changes( $order ) {
$has_meta_changes = false;
$order_meta = $order->get_meta_data();
$order_meta =
array_combine(
array_map( fn( $meta ) => $meta->id, $order_meta ),
$order_meta
);
// phpcs:disable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
foreach ( ( $_POST['meta'] ?? array() ) as $request_meta_id => $request_meta_data ) {
$request_meta_id = wp_unslash( $request_meta_id );
$request_meta_key = wp_unslash( $request_meta_data['key'] );
$request_meta_value = wp_unslash( $request_meta_data['value'] );
if ( array_key_exists( $request_meta_id, $order_meta ) &&
( $order_meta[ $request_meta_id ]->key !== $request_meta_key || $order_meta[ $request_meta_id ]->value !== $request_meta_value ) ) {
$order->update_meta_data( $request_meta_key, $request_meta_value, $request_meta_id );
$has_meta_changes = true;
}
}
$request_new_key = wp_unslash( $_POST['metakeyinput'] ?? '' );
$request_new_value = wp_unslash( $_POST['metavalue'] ?? '' );
if ( '' !== $request_new_key ) {
$order->add_meta_data( $request_new_key, $request_new_value );
$has_meta_changes = true;
}
// phpcs:enable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
if ( $has_meta_changes ) {
$order->save();
}
}
}

View File

@@ -290,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':
@@ -340,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.
*
@@ -351,6 +362,8 @@ class PageController {
$this->verify_edit_permission();
$this->handle_edit_lock();
$theorder = $this->order;
$this->prepare_order_edit_form();
}
/**
@@ -371,6 +384,7 @@ class PageController {
$this->order = new $order_class_name();
$this->order->set_object_read( false );
$this->order->set_status( 'auto-draft' );
$this->order->set_created_via( 'admin' );
$this->order->save();
$this->handle_edit_lock();
@@ -380,6 +394,8 @@ class PageController {
}
$theorder = $this->order;
$this->prepare_order_edit_form();
}
/**

View File

@@ -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' ),
),

View File

@@ -76,6 +76,7 @@ class DefaultFreeExtensions {
self::get_plugin( 'mailpoet' ),
self::get_plugin( 'google-listings-and-ads' ),
self::get_plugin( 'woocommerce-services:tax' ),
self::get_plugin( 'tiktok-for-business' ),
)
),
),
@@ -400,6 +401,16 @@ class DefaultFreeExtensions {
'value' => 'SE',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'JP',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'AE',
'operation' => '=',
),
),
),
DefaultPaymentGateways::get_rules_for_cbd( false ),
@@ -875,9 +886,9 @@ class DefaultFreeExtensions {
'install_priority' => 3,
),
'jetpack' => array(
'label' => __( 'Enhance security with Jetpack', 'woocommerce' ),
'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ),
'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ),
'description' => __( 'Save time on content creation — unlock high-quality blog posts and pages using AI.', 'woocommerce' ),
'learn_more_link' => 'https://woocommerce.com/products/jetpack',
'install_priority' => 8,
),

View File

@@ -40,7 +40,7 @@ class OrdersScheduler extends ImportScheduler {
// Order and refund data must be run on these hooks to ensure meta data is set.
add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) );
add_action( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
add_filter( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );
OrdersStatsDataStore::init();
@@ -210,13 +210,15 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
* @param int $order_id Post ID.
*
* @internal
* @returns int The order id
*/
public static function possibly_schedule_import( $order_id ) {
if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) {
return;
return $order_id;
}
self::schedule_action( 'import', array( $order_id ) );
return $order_id;
}
/**

View File

@@ -9,6 +9,7 @@ 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;
@@ -137,9 +138,7 @@ class Settings {
//phpcs:ignore
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
if ( class_exists( 'Jetpack' ) ) {
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
}
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
if ( ! empty( $preload_data_endpoints ) ) {
$preload_data = array_reduce(
array_values( $preload_data_endpoints ),
@@ -236,6 +235,8 @@ class Settings {
$settings['features'] = $this->get_features();
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
return $settings;
}

View File

@@ -81,6 +81,10 @@ class Translations {
continue;
}
if ( ! isset( $chunk_data['comment']['reference'] ) ) {
continue;
}
$reference_file = $chunk_data['comment']['reference'];
// Only combine "app" files (not scripts registered with WP).

View File

@@ -272,6 +272,7 @@ class WCAdminAssets {
'wc-store-data',
'wc-currency',
'wc-navigation',
'wc-block-templates',
'wc-product-editor',
);
@@ -297,6 +298,23 @@ class WCAdminAssets {
$script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' );
$script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename;
global $wp_version;
if ( 'app' === $script_path_name && version_compare( $wp_version, '6.3', '<' ) ) {
// Remove wp-router dependency for WordPress versions < 6.3 because wp-router is not included in those versions. We only use wp-router in customize store pages and the feature is only available in WordPress 6.3+.
// We can remove this once our minimum support is WP 6.3.
$script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-router' ) );
}
// Remove wp-editor dependency if we're not on a customize store page since we don't use wp-editor in other pages.
$is_customize_store_page = (
PageController::is_admin_page() &&
isset( $_GET['path'] ) &&
str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' )
);
if ( ! $is_customize_store_page && WC_ADMIN_APP === $script ) {
$script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-editor' ) );
}
wp_register_script(
$script,
self::get_url( $script_path_name . '/index', 'js' ),
@@ -330,6 +348,14 @@ class WCAdminAssets {
);
wp_style_add_data( 'wc-components', 'rtl', 'replace' );
wp_register_style(
'wc-block-templates',
self::get_url( 'block-templates/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-block-templates', 'rtl', 'replace' );
wp_register_style(
'wc-product-editor',
self::get_url( 'product-editor/style', 'css' ),
@@ -398,6 +424,7 @@ class WCAdminAssets {
'wc-date',
'wc-components',
'wc-tracks',
'wc-block-templates',
'wc-product-editor',
];
foreach ( $handles_for_injection as $handle ) {

View File

@@ -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.
*/

View File

@@ -12,7 +12,8 @@ use Automattic\WooCommerce\Admin\PageController;
* @package Automattic\WooCommerce\Admin\Features
*/
class WcPayWelcomePage {
const TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
const CACHE_TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
const HAD_WCPAY_OPTION_NAME = 'wcpay_was_in_use';
/**
* Plugin instance.
@@ -44,6 +45,7 @@ class WcPayWelcomePage {
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' ] );
add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] );
}
/**
@@ -52,6 +54,11 @@ class WcPayWelcomePage {
* @return boolean
*/
public function must_be_visible(): bool {
// The WooPayments plugin must not be active.
if ( $this->is_wcpay_active() ) {
return false;
}
// Suggestions not disabled via a setting.
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
return false;
@@ -68,12 +75,7 @@ class WcPayWelcomePage {
return false;
}
// The WooPayments plugin must not be active.
if ( $this->is_wcpay_active() ) {
return false;
}
// Incentive is available.
// An incentive must be available.
if ( empty( $this->get_incentive() ) ) {
return false;
}
@@ -133,11 +135,17 @@ class WcPayWelcomePage {
}
// 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;
}
}
}
@@ -154,8 +162,8 @@ class WcPayWelcomePage {
return $settings;
}
// Return early if there is no eligible incentive.
if ( empty( $this->get_incentive() ) ) {
// Return early if the incentive must not be visible.
if ( ! $this->must_be_visible() ) {
return $settings;
}
@@ -171,8 +179,8 @@ class WcPayWelcomePage {
* @return array
*/
public function allowed_promo_notes( $promo_notes = [] ): array {
// Return early if there is no eligible incentive.
if ( empty( $this->get_incentive() ) ) {
// Return early if the incentive must not be visible.
if ( ! $this->must_be_visible() ) {
return $promo_notes;
}
@@ -183,20 +191,50 @@ class WcPayWelcomePage {
}
/**
* Check if the WooPayments payment gateway is active and set up,
* Adds the WooPayments incentive badge to the onboarding task.
*
* @param string $badge Current badge.
*
* @return string
*/
public function onboarding_task_badge( string $badge ): string {
// Return early if the incentive must not be visible.
if ( ! $this->must_be_visible() ) {
return $badge;
}
return $this->get_incentive()['task_badge'] ?? $badge;
}
/**
* 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(): 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 is or was active at some point and that it was connected.
// 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() ) {
return true;
$had_wcpay = true;
}
// If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
if ( ! empty(
if ( false === $had_wcpay && ! empty(
wc_get_orders(
[
'payment_method' => 'woocommerce_payments',
@@ -205,10 +243,13 @@ class WcPayWelcomePage {
]
)
) ) {
return true;
$had_wcpay = true;
}
return false;
// Store the value for future use.
update_option( self::HAD_WCPAY_OPTION_NAME, $had_wcpay ? 'yes' : 'no' );
return $had_wcpay;
}
/**
@@ -240,14 +281,21 @@ class WcPayWelcomePage {
* @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.
if ( empty( $this->get_incentive() ) ) {
$incentive = $this->get_incentive();
if ( empty( $incentive ) ) {
return true;
}
$dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] );
if ( in_array( $this->get_incentive()['id'], $dismissed_incentives, true ) ) {
// Search the incentive ID in the dismissed incentives list.
if ( in_array( $incentive['id'], $dismissed_incentives, true ) ) {
return true;
}
@@ -265,6 +313,19 @@ class WcPayWelcomePage {
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(),
@@ -293,25 +354,16 @@ class WcPayWelcomePage {
// 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).
$transient_cache = get_transient( self::TRANSIENT_NAME );
if ( false !== $transient_cache ) {
if ( is_null( $transient_cache ) ) {
// This means there was an error and we shouldn't retry just yet.
// Initialize the in-memory cache.
$this->incentive = [];
} elseif ( ! empty( $transient_cache['context_hash'] ) && is_string( $transient_cache['context_hash'] )
&& hash_equals( $store_context_hash, $transient_cache['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.
$this->incentive = $transient_cache['incentive'] ?? [];
}
// 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'] ?? [];
// If the in-memory cache has been set, return it.
if ( isset( $this->incentive ) ) {
return $this->incentive;
}
return $this->incentive;
}
// By this point, we have an expired transient or the store context has changed.
@@ -323,16 +375,22 @@ class WcPayWelcomePage {
$response = wp_remote_get(
$url,
array(
[
'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 null value in the transient so we know this is due to an API error.
set_transient( self::TRANSIENT_NAME, null, HOUR_IN_SECONDS * 6 );
// Initialize the in-memory cache.
// 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;
@@ -362,19 +420,21 @@ class WcPayWelcomePage {
// 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 !== $transient_cache ) {
delete_transient( self::TRANSIENT_NAME );
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 24h.
// 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::TRANSIENT_NAME,
self::CACHE_TRANSIENT_NAME,
[
'incentive' => $this->incentive,
'context_hash' => $store_context_hash,
'timestamp' => time(),
],
! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS
);