update plugins
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Activity Panel.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the activity panel feature.
|
||||
*/
|
||||
class ActivityPanels {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var ActivityPanels instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
// Run after Automattic\WooCommerce\Internal\Admin\Loader.
|
||||
add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 );
|
||||
// New settings injection.
|
||||
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store activity panel last read and open times.
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
*/
|
||||
public function add_user_data_fields( $user_data_fields ) {
|
||||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'activity_panel_inbox_last_read',
|
||||
'activity_panel_reviews_last_read',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add alert count to the component settings.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
*/
|
||||
public function component_settings( $settings ) {
|
||||
$settings['alertCount'] = Notes::get_notes_count( array( 'error', 'update' ), array( 'unactioned' ) );
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Analytics.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Cache;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Analytics feature.
|
||||
*/
|
||||
class Analytics {
|
||||
/**
|
||||
* Option name used to toggle this feature.
|
||||
*/
|
||||
const TOGGLE_OPTION_NAME = 'woocommerce_analytics_enabled';
|
||||
/**
|
||||
* Clear cache tool identifier.
|
||||
*/
|
||||
const CACHE_TOOL_ID = 'clear_woocommerce_analytics_cache';
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Analytics instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Determines if the feature has been toggled on or off.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected static $is_updated = false;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
|
||||
add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
|
||||
|
||||
if ( ! Features::is_enabled( 'analytics' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_component_settings_preload_endpoints', array( $this, 'add_preload_endpoints' ) );
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ) );
|
||||
add_filter( 'woocommerce_debug_tools', array( $this, 'register_cache_clear_tool' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the feature toggle to the features settings.
|
||||
*
|
||||
* @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
|
||||
*
|
||||
* @param array $features Feature sections.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_feature_toggle( $features ) {
|
||||
return $features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page when the option is toggled to make sure all Analytics features are loaded.
|
||||
*
|
||||
* @param string $old_value Old value.
|
||||
* @param string $value New value.
|
||||
*/
|
||||
public static function reload_page_on_toggle( $old_value, $value ) {
|
||||
if ( $old_value === $value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$is_updated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the page if the setting has been updated.
|
||||
*/
|
||||
public static function maybe_reload_page() {
|
||||
if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload data from the countries endpoint.
|
||||
*
|
||||
* @param array $endpoints Array of preloaded endpoints.
|
||||
* @return array
|
||||
*/
|
||||
public function add_preload_endpoints( $endpoints ) {
|
||||
$endpoints['performanceIndicators'] = '/wc-analytics/reports/performance-indicators/allowed';
|
||||
$endpoints['leaderboards'] = '/wc-analytics/leaderboards/allowed';
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store user preferences for the columns to display on a report.
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
*/
|
||||
public function add_user_data_fields( $user_data_fields ) {
|
||||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'categories_report_columns',
|
||||
'coupons_report_columns',
|
||||
'customers_report_columns',
|
||||
'orders_report_columns',
|
||||
'products_report_columns',
|
||||
'revenue_report_columns',
|
||||
'taxes_report_columns',
|
||||
'variations_report_columns',
|
||||
'dashboard_sections',
|
||||
'dashboard_chart_type',
|
||||
'dashboard_chart_interval',
|
||||
'dashboard_leaderboard_rows',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the cache clearing tool on the WooCommerce > Status > Tools page.
|
||||
*
|
||||
* @param array $debug_tools Available debug tool registrations.
|
||||
* @return array Filtered debug tool registrations.
|
||||
*/
|
||||
public function register_cache_clear_tool( $debug_tools ) {
|
||||
$settings_url = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-admin',
|
||||
'path' => '/analytics/settings',
|
||||
),
|
||||
get_admin_url( null, 'admin.php' )
|
||||
);
|
||||
|
||||
$debug_tools[ self::CACHE_TOOL_ID ] = array(
|
||||
'name' => __( 'Clear analytics cache', 'woocommerce' ),
|
||||
'button' => __( 'Clear', 'woocommerce' ),
|
||||
'desc' => sprintf(
|
||||
/* translators: 1: opening link tag, 2: closing tag */
|
||||
__( 'This tool will reset the cached values used in WooCommerce Analytics. If numbers still look off, try %1$sReimporting Historical Data%2$s.', 'woocommerce' ),
|
||||
'<a href="' . esc_url( $settings_url ) . '">',
|
||||
'</a>'
|
||||
),
|
||||
'callback' => array( $this, 'run_clear_cache_tool' ),
|
||||
);
|
||||
|
||||
return $debug_tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers report pages.
|
||||
*/
|
||||
public function register_pages() {
|
||||
$report_pages = self::get_report_pages();
|
||||
foreach ( $report_pages as $report_page ) {
|
||||
if ( ! is_null( $report_page ) ) {
|
||||
wc_admin_register_page( $report_page );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get report pages.
|
||||
*/
|
||||
public static function get_report_pages() {
|
||||
$overview_page = array(
|
||||
'id' => 'woocommerce-analytics',
|
||||
'title' => __( 'Analytics', 'woocommerce' ),
|
||||
'path' => '/analytics/overview',
|
||||
'icon' => 'dashicons-chart-bar',
|
||||
'position' => 57, // After WooCommerce & Product menu items.
|
||||
);
|
||||
|
||||
$report_pages = array(
|
||||
$overview_page,
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-overview',
|
||||
'title' => __( 'Overview', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/overview',
|
||||
'nav_args' => array(
|
||||
'order' => 10,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-products',
|
||||
'title' => __( 'Products', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/products',
|
||||
'nav_args' => array(
|
||||
'order' => 20,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-revenue',
|
||||
'title' => __( 'Revenue', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/revenue',
|
||||
'nav_args' => array(
|
||||
'order' => 30,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-orders',
|
||||
'title' => __( 'Orders', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/orders',
|
||||
'nav_args' => array(
|
||||
'order' => 40,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-variations',
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/variations',
|
||||
'nav_args' => array(
|
||||
'order' => 50,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-categories',
|
||||
'title' => __( 'Categories', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/categories',
|
||||
'nav_args' => array(
|
||||
'order' => 60,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-coupons',
|
||||
'title' => __( 'Coupons', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/coupons',
|
||||
'nav_args' => array(
|
||||
'order' => 70,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-taxes',
|
||||
'title' => __( 'Taxes', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/taxes',
|
||||
'nav_args' => array(
|
||||
'order' => 80,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-downloads',
|
||||
'title' => __( 'Downloads', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/downloads',
|
||||
'nav_args' => array(
|
||||
'order' => 90,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
),
|
||||
'yes' === get_option( 'woocommerce_manage_stock' ) ? array(
|
||||
'id' => 'woocommerce-analytics-stock',
|
||||
'title' => __( 'Stock', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/stock',
|
||||
'nav_args' => array(
|
||||
'order' => 100,
|
||||
'parent' => 'woocommerce-analytics',
|
||||
),
|
||||
) : null,
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-customers',
|
||||
'title' => __( 'Customers', 'woocommerce' ),
|
||||
'parent' => 'woocommerce',
|
||||
'path' => '/customers',
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-settings',
|
||||
'title' => __( 'Settings', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/settings',
|
||||
'nav_args' => array(
|
||||
'title' => __( 'Analytics', 'woocommerce' ),
|
||||
'parent' => 'woocommerce-settings',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The analytics report items used in the menu.
|
||||
*
|
||||
* @since 6.4.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_analytics_report_menu_items', $report_pages );
|
||||
}
|
||||
|
||||
/**
|
||||
* "Clear" analytics cache by invalidating it.
|
||||
*/
|
||||
public function run_clear_cache_tool() {
|
||||
Cache::invalidate();
|
||||
|
||||
return __( 'Analytics cache cleared.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?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 = 10000;
|
||||
|
||||
/**
|
||||
* The block attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = array();
|
||||
|
||||
/**
|
||||
* The block hide conditions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $hide_conditions = array();
|
||||
|
||||
/**
|
||||
* The block hide conditions counter.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $hide_conditions_counter = 0;
|
||||
|
||||
/**
|
||||
* The block disable conditions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $disable_conditions = array();
|
||||
|
||||
/**
|
||||
* The block disable conditions counter.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $disable_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'] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::DISABLE_CONDITIONS_KEY ] ) ) {
|
||||
foreach ( $config[ self::DISABLE_CONDITIONS_KEY ] as $disable_condition ) {
|
||||
$this->add_disable_condition( $disable_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a block attribute value without replacing the entire attributes object.
|
||||
*
|
||||
* @param string $key The attribute key.
|
||||
* @param mixed $value The attribute value.
|
||||
*/
|
||||
public function set_attribute( string $key, $value ) {
|
||||
$this->attributes[ $key ] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ] = array(
|
||||
'expression' => $expression,
|
||||
);
|
||||
|
||||
/**
|
||||
* Action called after a hide condition is added to a block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_after_add_hide_condition', $this );
|
||||
|
||||
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 ] );
|
||||
|
||||
/**
|
||||
* Action called after a hide condition is removed from a block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_after_remove_hide_condition', $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hide conditions of the block.
|
||||
*/
|
||||
public function get_hide_conditions(): array {
|
||||
return $this->hide_conditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a disable condition to the block.
|
||||
*
|
||||
* The disable 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 disable the block.
|
||||
*/
|
||||
public function add_disable_condition( string $expression ): string {
|
||||
$key = 'k' . $this->disable_conditions_counter;
|
||||
$this->disable_conditions_counter++;
|
||||
|
||||
// Storing the expression in an array to allow for future expansion
|
||||
// (such as adding the plugin that added the condition).
|
||||
$this->disable_conditions[ $key ] = array(
|
||||
'expression' => $expression,
|
||||
);
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a disable condition from the block.
|
||||
*
|
||||
* @param string $key The key of the disable condition to remove.
|
||||
*/
|
||||
public function remove_disable_condition( string $key ) {
|
||||
unset( $this->disable_conditions[ $key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disable conditions of the block.
|
||||
*/
|
||||
public function get_disable_conditions(): array {
|
||||
return $this->disable_conditions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template as JSON like array.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
public function to_json(): array {
|
||||
return array(
|
||||
'id' => $this->get_id(),
|
||||
'title' => $this->get_title(),
|
||||
'description' => $this->get_description(),
|
||||
'area' => $this->get_area(),
|
||||
'blockTemplates' => $this->get_formatted_template(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?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 = array();
|
||||
|
||||
// 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 ) {
|
||||
$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;
|
||||
}
|
||||
);
|
||||
|
||||
$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->do_after_add_block_error_action( $block, 'woocommerce_block_template_after_add_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->do_after_add_block_error_action( $block, "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}", $e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_after_add_block_error` action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
* @param string $action The action that threw the exception.
|
||||
* @param \Exception $e The exception.
|
||||
*/
|
||||
private function do_after_add_block_error_action( BlockInterface $block, string $action, \Exception $e ) {
|
||||
/**
|
||||
* Action called after an exception is thrown by a `woocommerce_block_template_after_add_block` action hook.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
* @param string $action The action that threw the exception.
|
||||
* @param \Exception $exception The exception.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
do_action(
|
||||
'woocommerce_block_template_after_add_block_error',
|
||||
$block,
|
||||
$action,
|
||||
$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->do_after_remove_block_error_action( $block, 'woocommerce_block_template_after_remove_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->do_after_remove_block_error_action( $block, "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}", $e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_after_remove_block_error` action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
* @param string $action The action that threw the exception.
|
||||
* @param \Exception $e The exception.
|
||||
*/
|
||||
private function do_after_remove_block_error_action( BlockInterface $block, string $action, \Exception $e ) {
|
||||
/**
|
||||
* Action called after an exception is thrown by a `woocommerce_block_template_after_remove_block` action hook.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
* @param string $action The action that threw the exception.
|
||||
* @param \Exception $exception The exception.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
do_action(
|
||||
'woocommerce_block_template_after_remove_block_error',
|
||||
$block,
|
||||
$action,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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 = array(
|
||||
$this->get_name(),
|
||||
array_merge(
|
||||
$this->get_attributes(),
|
||||
array(
|
||||
'_templateBlockId' => $this->get_id(),
|
||||
'_templateBlockOrder' => $this->get_order(),
|
||||
),
|
||||
! empty( $this->get_hide_conditions() ) ? array(
|
||||
'_templateBlockHideConditions' => $this->get_formatted_hide_conditions(),
|
||||
) : array(),
|
||||
! empty( $this->get_disable_conditions() ) ? array(
|
||||
'_templateBlockDisableConditions' => $this->get_formatted_disable_conditions(),
|
||||
) : array(),
|
||||
),
|
||||
);
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block hide conditions formatted for inclusion in a formatted template.
|
||||
*/
|
||||
private function get_formatted_hide_conditions(): array {
|
||||
return $this->format_conditions( $this->get_hide_conditions() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block disable conditions formatted for inclusion in a formatted template.
|
||||
*/
|
||||
private function get_formatted_disable_conditions(): array {
|
||||
return $this->format_conditions( $this->get_disable_conditions() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats conditions in the expected format to include in the template.
|
||||
*
|
||||
* @param array $conditions The conditions to format.
|
||||
*/
|
||||
private function format_conditions( $conditions ): array {
|
||||
$formatted_expressions = array_map(
|
||||
function( $condition ) {
|
||||
return array(
|
||||
'expression' => $condition['expression'],
|
||||
);
|
||||
},
|
||||
array_values( $conditions )
|
||||
);
|
||||
|
||||
return $formatted_expressions;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
<?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;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Logger for block template modifications.
|
||||
*/
|
||||
class BlockTemplateLogger {
|
||||
const BLOCK_ADDED = 'block_added';
|
||||
const BLOCK_REMOVED = 'block_removed';
|
||||
const BLOCK_MODIFIED = 'block_modified';
|
||||
const BLOCK_ADDED_TO_DETACHED_CONTAINER = 'block_added_to_detached_container';
|
||||
const HIDE_CONDITION_ADDED = 'hide_condition_added';
|
||||
const HIDE_CONDITION_REMOVED = 'hide_condition_removed';
|
||||
const HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK = 'hide_condition_added_to_detached_block';
|
||||
const ERROR_AFTER_BLOCK_ADDED = 'error_after_block_added';
|
||||
const ERROR_AFTER_BLOCK_REMOVED = 'error_after_block_removed';
|
||||
|
||||
const LOG_HASH_TRANSIENT_BASE_NAME = 'wc_block_template_events_log_hash_';
|
||||
|
||||
/**
|
||||
* Event types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $event_types = array(
|
||||
self::BLOCK_ADDED => array(
|
||||
'level' => \WC_Log_Levels::DEBUG,
|
||||
'message' => 'Block added to template.',
|
||||
),
|
||||
self::BLOCK_REMOVED => array(
|
||||
'level' => \WC_Log_Levels::NOTICE,
|
||||
'message' => 'Block removed from template.',
|
||||
),
|
||||
self::BLOCK_MODIFIED => array(
|
||||
'level' => \WC_Log_Levels::NOTICE,
|
||||
'message' => 'Block modified in template.',
|
||||
),
|
||||
self::BLOCK_ADDED_TO_DETACHED_CONTAINER => array(
|
||||
'level' => \WC_Log_Levels::WARNING,
|
||||
'message' => 'Block added to detached container. Block will not be included in the template, since the container will not be included in the template.',
|
||||
),
|
||||
self::HIDE_CONDITION_ADDED => array(
|
||||
'level' => \WC_Log_Levels::NOTICE,
|
||||
'message' => 'Hide condition added to block.',
|
||||
),
|
||||
self::HIDE_CONDITION_REMOVED => array(
|
||||
'level' => \WC_Log_Levels::NOTICE,
|
||||
'message' => 'Hide condition removed from block.',
|
||||
),
|
||||
self::HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK => array(
|
||||
'level' => \WC_Log_Levels::WARNING,
|
||||
'message' => 'Hide condition added to detached block. Block will not be included in the template, so the hide condition is not needed.',
|
||||
),
|
||||
self::ERROR_AFTER_BLOCK_ADDED => array(
|
||||
'level' => \WC_Log_Levels::WARNING,
|
||||
'message' => 'Error after block added to template.',
|
||||
),
|
||||
self::ERROR_AFTER_BLOCK_REMOVED => array(
|
||||
'level' => \WC_Log_Levels::WARNING,
|
||||
'message' => 'Error after block removed from template.',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var BlockTemplateLogger
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Logger instance.
|
||||
*
|
||||
* @var \WC_Logger
|
||||
*/
|
||||
protected $logger = null;
|
||||
|
||||
/**
|
||||
* All template events.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $all_template_events = array();
|
||||
|
||||
/**
|
||||
* Templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $templates = array();
|
||||
|
||||
/**
|
||||
* Threshold severity.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $threshold_severity = 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();
|
||||
|
||||
$threshold = get_option( 'woocommerce_block_template_logging_threshold', \WC_Log_Levels::WARNING );
|
||||
if ( ! \WC_Log_Levels::is_valid_level( $threshold ) ) {
|
||||
$threshold = \WC_Log_Levels::INFO;
|
||||
}
|
||||
|
||||
$this->threshold_severity = \WC_Log_Levels::get_level_severity( $threshold );
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_add_block',
|
||||
function ( BlockInterface $block ) {
|
||||
$is_detached = method_exists( $block->get_parent(), 'is_detached' ) && $block->get_parent()->is_detached();
|
||||
|
||||
$this->log(
|
||||
$is_detached
|
||||
? $this::BLOCK_ADDED_TO_DETACHED_CONTAINER
|
||||
: $this::BLOCK_ADDED,
|
||||
$block,
|
||||
);
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_remove_block',
|
||||
function ( BlockInterface $block ) {
|
||||
$this->log(
|
||||
$this::BLOCK_REMOVED,
|
||||
$block,
|
||||
);
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_add_hide_condition',
|
||||
function ( BlockInterface $block ) {
|
||||
$this->log(
|
||||
$block->is_detached()
|
||||
? $this::HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK
|
||||
: $this::HIDE_CONDITION_ADDED,
|
||||
$block,
|
||||
);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_remove_hide_condition',
|
||||
function ( BlockInterface $block ) {
|
||||
$this->log(
|
||||
$this::HIDE_CONDITION_REMOVED,
|
||||
$block,
|
||||
);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_add_block_error',
|
||||
function ( BlockInterface $block, string $action, \Exception $exception ) {
|
||||
$this->log(
|
||||
$this::ERROR_AFTER_BLOCK_ADDED,
|
||||
$block,
|
||||
array(
|
||||
'action' => $action,
|
||||
'exception' => $exception,
|
||||
),
|
||||
);
|
||||
},
|
||||
0,
|
||||
3
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_block_template_after_remove_block_error',
|
||||
function ( BlockInterface $block, string $action, \Exception $exception ) {
|
||||
$this->log(
|
||||
$this::ERROR_AFTER_BLOCK_REMOVED,
|
||||
$block,
|
||||
array(
|
||||
'action' => $action,
|
||||
'exception' => $exception,
|
||||
),
|
||||
);
|
||||
},
|
||||
0,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events for a given template as a JSON like array.
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
*/
|
||||
public function template_events_to_json( string $template_id ): array {
|
||||
if ( ! isset( $this->all_template_events[ $template_id ] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$template_events = $this->all_template_events[ $template_id ];
|
||||
|
||||
return $this->to_json( $template_events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events as a JSON like array.
|
||||
*
|
||||
* @param array $template_events Template events.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
private function to_json( array $template_events ): array {
|
||||
$json = array();
|
||||
|
||||
foreach ( $template_events as $template_event ) {
|
||||
$container = $template_event['container'];
|
||||
$block = $template_event['block'];
|
||||
|
||||
$json[] = array(
|
||||
'level' => $template_event['level'],
|
||||
'event_type' => $template_event['event_type'],
|
||||
'message' => $template_event['message'],
|
||||
'container' => $container instanceof BlockInterface
|
||||
? array(
|
||||
'id' => $container->get_id(),
|
||||
'name' => $container->get_name(),
|
||||
)
|
||||
: null,
|
||||
'block' => array(
|
||||
'id' => $block->get_id(),
|
||||
'name' => $block->get_name(),
|
||||
),
|
||||
'additional_info' => $this->format_info( $template_event['additional_info'] ),
|
||||
);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log all template events for a given template to the log file.
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
*/
|
||||
public function log_template_events_to_file( string $template_id ) {
|
||||
if ( ! isset( $this->all_template_events[ $template_id ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template_events = $this->all_template_events[ $template_id ];
|
||||
|
||||
$hash = $this->generate_template_events_hash( $template_events );
|
||||
|
||||
if ( ! $this->has_template_events_changed( $template_id, $hash ) ) {
|
||||
// Nothing has changed since the last time this was logged,
|
||||
// so don't log it again.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set_template_events_log_hash( $template_id, $hash );
|
||||
|
||||
$template = $this->templates[ $template_id ];
|
||||
|
||||
foreach ( $template_events as $template_event ) {
|
||||
$info = array_merge(
|
||||
array(
|
||||
'template' => $template,
|
||||
'container' => $template_event['container'],
|
||||
'block' => $template_event['block'],
|
||||
),
|
||||
$template_event['additional_info']
|
||||
);
|
||||
|
||||
$message = $this->format_message( $template_event['message'], $info );
|
||||
|
||||
$this->logger->log(
|
||||
$template_event['level'],
|
||||
$message,
|
||||
array( 'source' => 'block_template' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the template events changed since the last time they were logged?
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
* @param string $events_hash Events hash.
|
||||
*/
|
||||
private function has_template_events_changed( string $template_id, string $events_hash ) {
|
||||
$previous_hash = get_transient( self::LOG_HASH_TRANSIENT_BASE_NAME . $template_id );
|
||||
|
||||
return $previous_hash !== $events_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for a given set of template events.
|
||||
*
|
||||
* @param array $template_events Template events.
|
||||
*/
|
||||
private function generate_template_events_hash( array $template_events ): string {
|
||||
return md5( wp_json_encode( $this->to_json( $template_events ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the template events hash for a given template.
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
* @param string $hash Hash of template events.
|
||||
*/
|
||||
private function set_template_events_log_hash( string $template_id, string $hash ) {
|
||||
set_transient( self::LOG_HASH_TRANSIENT_BASE_NAME . $template_id, $hash );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an event.
|
||||
*
|
||||
* @param string $event_type Event type.
|
||||
* @param BlockInterface $block Block.
|
||||
* @param array $additional_info Additional info.
|
||||
*/
|
||||
private function log( string $event_type, BlockInterface $block, $additional_info = array() ) {
|
||||
if ( ! isset( self::$event_types[ $event_type ] ) ) {
|
||||
/* translators: 1: WC_Logger::log 2: level */
|
||||
wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid event type "%2$s".', 'woocommerce' ), '<code>BlockTemplateLogger::log</code>', $event_type ), '8.4' );
|
||||
}
|
||||
|
||||
$event_type_info = isset( self::$event_types[ $event_type ] )
|
||||
? array_merge(
|
||||
self::$event_types[ $event_type ],
|
||||
array(
|
||||
'event_type' => $event_type,
|
||||
)
|
||||
)
|
||||
: array(
|
||||
'level' => \WC_Log_Levels::ERROR,
|
||||
'event_type' => $event_type,
|
||||
'message' => 'Unknown error.',
|
||||
);
|
||||
|
||||
if ( ! $this->should_handle( $event_type_info['level'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template = $block->get_root_template();
|
||||
$container = $block->get_parent();
|
||||
|
||||
$this->add_template_event( $event_type_info, $template, $container, $block, $additional_info );
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the logger handle a given level?
|
||||
*
|
||||
* @param int $level Level to check.
|
||||
*/
|
||||
private function should_handle( $level ) {
|
||||
return $this->threshold_severity <= \WC_Log_Levels::get_level_severity( $level );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template event.
|
||||
*
|
||||
* @param array $event_type_info Event type info.
|
||||
* @param BlockTemplateInterface $template Template.
|
||||
* @param ContainerInterface $container Container.
|
||||
* @param BlockInterface $block Block.
|
||||
* @param array $additional_info Additional info.
|
||||
*/
|
||||
private function add_template_event( array $event_type_info, BlockTemplateInterface $template, ContainerInterface $container, BlockInterface $block, array $additional_info = array() ) {
|
||||
$template_id = $template->get_id();
|
||||
|
||||
if ( ! isset( $this->all_template_events[ $template_id ] ) ) {
|
||||
$this->all_template_events[ $template_id ] = array();
|
||||
$this->templates[ $template_id ] = $template;
|
||||
}
|
||||
|
||||
$template_events = &$this->all_template_events[ $template_id ];
|
||||
|
||||
$template_events[] = array(
|
||||
'level' => $event_type_info['level'],
|
||||
'event_type' => $event_type_info['event_type'],
|
||||
'message' => $event_type_info['message'],
|
||||
'container' => $container,
|
||||
'block' => $block,
|
||||
'additional_info' => $additional_info,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = array() ): 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 array(
|
||||
'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 = array();
|
||||
|
||||
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()})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
/**
|
||||
* Keeps the product category lookup table in sync with live data.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* \Automattic\WooCommerce\Internal\Admin\CategoryLookup class.
|
||||
*/
|
||||
class CategoryLookup {
|
||||
|
||||
/**
|
||||
* Stores changes to categories we need to sync.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $edited_product_cats = array();
|
||||
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __construct() {}
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*
|
||||
* @return object Instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( null === static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'generate_category_lookup_table', array( $this, 'regenerate' ) );
|
||||
add_action( 'edit_product_cat', array( $this, 'before_edit' ), 99 );
|
||||
add_action( 'edited_product_cat', array( $this, 'on_edit' ), 99 );
|
||||
add_action( 'created_product_cat', array( $this, 'on_create' ), 99 );
|
||||
add_action( 'init', array( $this, 'define_category_lookup_tables_in_wpdb' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate all lookup table data.
|
||||
*/
|
||||
public function regenerate() {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "TRUNCATE TABLE $wpdb->wc_category_lookup" );
|
||||
|
||||
$terms = get_terms(
|
||||
'product_cat',
|
||||
array(
|
||||
'hide_empty' => false,
|
||||
'fields' => 'id=>parent',
|
||||
)
|
||||
);
|
||||
|
||||
$hierarchy = array();
|
||||
$inserts = array();
|
||||
|
||||
$this->unflatten_terms( $hierarchy, $terms, 0 );
|
||||
$this->get_term_insert_values( $inserts, $hierarchy );
|
||||
|
||||
if ( ! $inserts ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$insert_string = implode(
|
||||
'),(',
|
||||
array_map(
|
||||
function( $item ) {
|
||||
return implode( ',', $item );
|
||||
},
|
||||
$inserts
|
||||
)
|
||||
);
|
||||
|
||||
$wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_tree_id,category_id) VALUES ({$insert_string})" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Store edits so we know when the parent ID changes.
|
||||
*
|
||||
* @param int $category_id Term ID being edited.
|
||||
*/
|
||||
public function before_edit( $category_id ) {
|
||||
$category = get_term( $category_id, 'product_cat' );
|
||||
$this->edited_product_cats[ $category_id ] = $category->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a product category gets edited, see if we need to sync the table.
|
||||
*
|
||||
* @param int $category_id Term ID being edited.
|
||||
*/
|
||||
public function on_edit( $category_id ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! isset( $this->edited_product_cats[ $category_id ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$category_object = get_term( $category_id, 'product_cat' );
|
||||
$prev_parent = $this->edited_product_cats[ $category_id ];
|
||||
$new_parent = $category_object->parent;
|
||||
|
||||
// No edits - no need to modify relationships.
|
||||
if ( $prev_parent === $new_parent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->delete( $category_id, $prev_parent );
|
||||
$this->update( $category_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* When a product category gets created, add a new lookup row.
|
||||
*
|
||||
* @param int $category_id Term ID being created.
|
||||
*/
|
||||
public function on_create( $category_id ) {
|
||||
// If WooCommerce is being installed on a multisite, lookup tables haven't been created yet.
|
||||
if ( 'yes' === get_transient( 'wc_installing' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->update( $category_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete lookup table data from a tree.
|
||||
*
|
||||
* @param int $category_id Category ID to delete.
|
||||
* @param int $category_tree_id Tree to delete from.
|
||||
* @return void
|
||||
*/
|
||||
protected function delete( $category_id, $category_tree_id ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $category_tree_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ancestors = get_ancestors( $category_tree_id, 'product_cat', 'taxonomy' );
|
||||
$ancestors[] = $category_tree_id;
|
||||
$children = get_term_children( $category_id, 'product_cat' );
|
||||
$children[] = $category_id;
|
||||
$id_list = implode( ',', array_map( 'intval', array_unique( array_filter( $children ) ) ) );
|
||||
|
||||
foreach ( $ancestors as $ancestor ) {
|
||||
$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d AND category_id IN ({$id_list})", $ancestor ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lookup table data for a category by ID.
|
||||
*
|
||||
* @param int $category_id Category ID to update.
|
||||
*/
|
||||
protected function update( $category_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$ancestors = get_ancestors( $category_id, 'product_cat', 'taxonomy' );
|
||||
$children = get_term_children( $category_id, 'product_cat' );
|
||||
$inserts = array();
|
||||
$inserts[] = $this->get_insert_sql( $category_id, $category_id );
|
||||
$children_ids = array_map( 'intval', array_unique( array_filter( $children ) ) );
|
||||
|
||||
foreach ( $ancestors as $ancestor ) {
|
||||
$inserts[] = $this->get_insert_sql( $category_id, $ancestor );
|
||||
|
||||
foreach ( $children_ids as $child_category_id ) {
|
||||
$inserts[] = $this->get_insert_sql( $child_category_id, $ancestor );
|
||||
}
|
||||
}
|
||||
|
||||
$insert_string = implode( ',', $inserts );
|
||||
|
||||
$wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_id, category_tree_id) VALUES {$insert_string}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category lookup table values to insert.
|
||||
*
|
||||
* @param int $category_id Category ID to insert.
|
||||
* @param int $category_tree_id Tree to insert into.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_insert_sql( $category_id, $category_tree_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->prepare( '(%d,%d)', $category_id, $category_tree_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to construct insert query recursively.
|
||||
*
|
||||
* @param array $inserts Array of data to insert.
|
||||
* @param array $terms Terms to insert.
|
||||
* @param array $parents Parent IDs the terms belong to.
|
||||
*/
|
||||
protected function get_term_insert_values( &$inserts, $terms, $parents = array() ) {
|
||||
foreach ( $terms as $term ) {
|
||||
$insert_parents = array_merge( array( $term['term_id'] ), $parents );
|
||||
|
||||
foreach ( $insert_parents as $parent ) {
|
||||
$inserts[] = array(
|
||||
$parent,
|
||||
$term['term_id'],
|
||||
);
|
||||
}
|
||||
|
||||
$this->get_term_insert_values( $inserts, $term['descendants'], $insert_parents );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert flat terms array into nested array.
|
||||
*
|
||||
* @param array $hierarchy Array to put terms into.
|
||||
* @param array $terms Array of terms (id=>parent).
|
||||
* @param integer $parent Parent ID.
|
||||
*/
|
||||
protected function unflatten_terms( &$hierarchy, &$terms, $parent = 0 ) {
|
||||
foreach ( $terms as $term_id => $parent_id ) {
|
||||
if ( (int) $parent_id === $parent ) {
|
||||
$hierarchy[ $term_id ] = array(
|
||||
'term_id' => $term_id,
|
||||
'descendants' => array(),
|
||||
);
|
||||
unset( $terms[ $term_id ] );
|
||||
}
|
||||
}
|
||||
foreach ( $hierarchy as $term_id => $terms_array ) {
|
||||
$this->unflatten_terms( $hierarchy[ $term_id ]['descendants'], $terms, $term_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category descendants.
|
||||
*
|
||||
* @param int $category_id The category ID to lookup.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_descendants( $category_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return wp_parse_id_list(
|
||||
$wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT category_id FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d",
|
||||
$category_id
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all ancestor category ids for a category.
|
||||
*
|
||||
* @param int $category_id The category ID to lookup.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_ancestors( $category_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return wp_parse_id_list(
|
||||
$wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT category_tree_id FROM $wpdb->wc_category_lookup WHERE category_id = %d",
|
||||
$category_id
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add category lookup table to $wpdb object.
|
||||
*/
|
||||
public static function define_category_lookup_tables_in_wpdb() {
|
||||
global $wpdb;
|
||||
|
||||
// List of tables without prefixes.
|
||||
$tables = array(
|
||||
'wc_category_lookup' => 'wc_category_lookup',
|
||||
);
|
||||
|
||||
foreach ( $tables as $name => $table ) {
|
||||
$wpdb->$name = $wpdb->prefix . $table;
|
||||
$wpdb->tables[] = $table;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Coupons.php
Normal file
123
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Coupons.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Marketing > Coupons.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Coupons feature.
|
||||
*/
|
||||
class Coupons {
|
||||
|
||||
use CouponsMovedTrait;
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Coupons instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the main marketing feature is disabled, don't modify coupon behavior.
|
||||
if ( ! Features::is_enabled( 'marketing' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only support coupon modifications if coupons are enabled.
|
||||
if ( ! wc_coupons_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
( new CouponPageMoved() )->init();
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_add_marketing_coupon_script' ) );
|
||||
add_action( 'woocommerce_register_post_type_shop_coupon', array( $this, 'move_coupons' ) );
|
||||
add_action( 'admin_head', array( $this, 'fix_coupon_menu_highlight' ), 99 );
|
||||
add_action( 'admin_menu', array( $this, 'maybe_add_coupon_menu_redirect' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add menu item back in original spot to help people transition
|
||||
*/
|
||||
public function maybe_add_coupon_menu_redirect() {
|
||||
if ( ! $this->should_display_legacy_menu() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__( 'Coupons', 'woocommerce' ),
|
||||
__( 'Coupons', 'woocommerce' ),
|
||||
'manage_options',
|
||||
'coupons-moved',
|
||||
array( $this, 'coupon_menu_moved' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call back for transition menu item
|
||||
*/
|
||||
public function coupon_menu_moved() {
|
||||
wp_safe_redirect( $this->get_legacy_coupon_url(), 301 );
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify registered post type shop_coupon
|
||||
*
|
||||
* @param array $args Array of post type parameters.
|
||||
*
|
||||
* @return array the filtered parameters.
|
||||
*/
|
||||
public function move_coupons( $args ) {
|
||||
$args['show_in_menu'] = current_user_can( 'manage_woocommerce' ) ? 'woocommerce-marketing' : true;
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo WC modifications to $parent_file for 'shop_coupon'
|
||||
*/
|
||||
public function fix_coupon_menu_highlight() {
|
||||
global $parent_file, $post_type;
|
||||
|
||||
if ( $post_type === 'shop_coupon' ) {
|
||||
$parent_file = 'woocommerce-marketing'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add our wc-admin coupon scripts if viewing coupon pages
|
||||
*/
|
||||
public function maybe_add_marketing_coupon_script() {
|
||||
$curent_screen = PageController::get_instance()->get_current_page();
|
||||
if ( ! isset( $curent_screen['id'] ) || $curent_screen['id'] !== 'woocommerce-coupons' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WCAdminAssets::register_style( 'marketing-coupons', 'style' );
|
||||
WCAdminAssets::register_script( 'wp-admin-scripts', 'marketing-coupons', true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* A Trait to help with managing the legacy coupon menu.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* CouponsMovedTrait trait.
|
||||
*/
|
||||
trait CouponsMovedTrait {
|
||||
|
||||
/**
|
||||
* The GET query key for the legacy menu.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $query_key = 'legacy_coupon_menu';
|
||||
|
||||
/**
|
||||
* The key for storing an option in the DB.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $option_key = 'wc_admin_show_legacy_coupon_menu';
|
||||
|
||||
/**
|
||||
* Get the URL for the legacy coupon management.
|
||||
*
|
||||
* @return string The unescaped URL for the legacy coupon management page.
|
||||
*/
|
||||
protected static function get_legacy_coupon_url() {
|
||||
return self::get_coupon_url( [ self::$query_key => true ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the coupon management page.
|
||||
*
|
||||
* @param array $args Additional URL query arguments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_coupon_url( $args = [] ) {
|
||||
$args = array_merge(
|
||||
[
|
||||
'post_type' => 'shop_coupon',
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
return add_query_arg( $args, admin_url( 'edit.php' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the new URL for managing coupons.
|
||||
*
|
||||
* @param string $page The management page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_management_url( $page ) {
|
||||
$path = '';
|
||||
switch ( $page ) {
|
||||
case 'coupon':
|
||||
case 'coupons':
|
||||
return self::get_coupon_url();
|
||||
|
||||
case 'marketing':
|
||||
$path = self::get_marketing_path();
|
||||
break;
|
||||
}
|
||||
|
||||
return "wc-admin&path={$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WC Admin path for the marking page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_marketing_path() {
|
||||
return '/marketing/overview';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should display the legacy coupon menu item.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function should_display_legacy_menu() {
|
||||
return ( get_option( self::$option_key, 1 ) && ! Features::is_enabled( 'navigation' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we should display the legacy coupon menu item.
|
||||
*
|
||||
* @param bool $display Whether the menu should be displayed or not.
|
||||
*/
|
||||
protected static function display_legacy_menu( $display = false ) {
|
||||
update_option( self::$option_key, $display ? 1 : 0 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Customer effort score tracks
|
||||
*
|
||||
* @package WooCommerce\Admin\Features
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Triggers customer effort score on several different actions.
|
||||
*/
|
||||
class CustomerEffortScoreTracks {
|
||||
/**
|
||||
* Option name for the CES Tracks queue.
|
||||
*/
|
||||
const CES_TRACKS_QUEUE_OPTION_NAME = 'woocommerce_ces_tracks_queue';
|
||||
|
||||
/**
|
||||
* Option name for the clear CES Tracks queue for page.
|
||||
*/
|
||||
const CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME =
|
||||
'woocommerce_clear_ces_tracks_queue_for_page';
|
||||
|
||||
/**
|
||||
* Option name for the set of actions that have been shown.
|
||||
*/
|
||||
const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
|
||||
|
||||
/**
|
||||
* Action name for product add/publish.
|
||||
*/
|
||||
const PRODUCT_ADD_PUBLISH_ACTION_NAME = 'product_add_publish';
|
||||
|
||||
/**
|
||||
* Action name for product update.
|
||||
*/
|
||||
const PRODUCT_UPDATE_ACTION_NAME = 'product_update';
|
||||
|
||||
/**
|
||||
* Action name for shop order update.
|
||||
*/
|
||||
const SHOP_ORDER_UPDATE_ACTION_NAME = 'shop_order_update';
|
||||
|
||||
/**
|
||||
* Action name for settings change.
|
||||
*/
|
||||
const SETTINGS_CHANGE_ACTION_NAME = 'settings_change';
|
||||
|
||||
/**
|
||||
* Action name for add product categories.
|
||||
*/
|
||||
const ADD_PRODUCT_CATEGORIES_ACTION_NAME = 'add_product_categories';
|
||||
|
||||
/**
|
||||
* Action name for add product tags.
|
||||
*/
|
||||
const ADD_PRODUCT_TAGS_ACTION_NAME = 'add_product_tags';
|
||||
|
||||
/*
|
||||
* Action name for add product attributes.
|
||||
*/
|
||||
const ADD_PRODUCT_ATTRIBUTES_ACTION_NAME = 'add_product_attributes';
|
||||
|
||||
/**
|
||||
* Action name for import products.
|
||||
*/
|
||||
const IMPORT_PRODUCTS_ACTION_NAME = 'import_products';
|
||||
|
||||
/**
|
||||
* Action name for search.
|
||||
*/
|
||||
const SEARCH_ACTION_NAME = 'ces_search';
|
||||
|
||||
/**
|
||||
* Label for the snackbar that appears when a user submits the survey.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $onsubmit_label;
|
||||
|
||||
/**
|
||||
* Constructor. Sets up filters to hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->enable_survey_enqueing_if_tracking_is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add actions that require woocommerce_allow_tracking.
|
||||
*/
|
||||
private function enable_survey_enqueing_if_tracking_is_enabled() {
|
||||
// Only hook up the action handlers if in wp-admin.
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not hook up the action handlers if a mobile device is used.
|
||||
if ( wp_is_mobile() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only enqueue a survey if tracking is allowed.
|
||||
$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking', 'no' );
|
||||
if ( ! $allow_tracking ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_init', array( $this, 'maybe_clear_ces_tracks_queue' ) );
|
||||
add_action( 'woocommerce_update_options', array( $this, 'run_on_update_options' ), 10, 3 );
|
||||
add_action( 'product_cat_add_form', array( $this, 'add_script_track_product_categories' ), 10, 3 );
|
||||
add_action( 'product_tag_add_form', array( $this, 'add_script_track_product_tags' ), 10, 3 );
|
||||
add_action( 'woocommerce_attribute_added', array( $this, 'run_on_add_product_attributes' ), 10, 3 );
|
||||
add_action( 'load-edit.php', array( $this, 'run_on_load_edit_php' ), 10, 3 );
|
||||
add_action( 'product_page_product_importer', array( $this, 'run_on_product_import' ), 10, 3 );
|
||||
// Only hook up the transition_post_status action handler
|
||||
// if on the edit page.
|
||||
global $pagenow;
|
||||
if ( 'post.php' === $pagenow ) {
|
||||
add_action(
|
||||
'transition_post_status',
|
||||
array(
|
||||
$this,
|
||||
'run_on_transition_post_status',
|
||||
),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
$this->onsubmit_label = __( 'Thank you for your feedback!', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a generated script for tracking tags added on edit-tags.php page.
|
||||
* CES survey is triggered via direct access to wc/customer-effort-score store
|
||||
* via wp.data.dispatch method.
|
||||
*
|
||||
* Due to lack of options to directly hook ourselves into the ajax post request
|
||||
* initiated by edit-tags.php page, we infer a successful request by observing
|
||||
* an increase of the number of rows in tags table
|
||||
*
|
||||
* @param string $action Action name for the survey.
|
||||
* @param string $title Title for the snackbar.
|
||||
* @param string $first_question The text for the first question.
|
||||
* @param string $second_question The text for the second question.
|
||||
*
|
||||
* @return string Generated JavaScript to append to page.
|
||||
*/
|
||||
private function get_script_track_edit_php( $action, $title, $first_question, $second_question ) {
|
||||
return sprintf(
|
||||
"(function( $ ) {
|
||||
'use strict';
|
||||
// Hook on submit button and sets a 500ms interval function
|
||||
// to determine successful add tag or otherwise.
|
||||
$('#addtag #submit').on( 'click', function() {
|
||||
const initialCount = $('.tags tbody > tr').length;
|
||||
const interval = setInterval( function() {
|
||||
if ( $('.tags tbody > tr').length > initialCount ) {
|
||||
// New tag detected.
|
||||
clearInterval( interval );
|
||||
wp.data.dispatch('wc/customer-effort-score').addCesSurvey({ action: '%s', title: '%s', firstQuestion: '%s', secondQuestion: '%s', onsubmitLabel: '%s' });
|
||||
} else {
|
||||
// Form is no longer loading, most likely failed.
|
||||
if ( $( '#addtag .submit .spinner.is-active' ).length < 1 ) {
|
||||
clearInterval( interval );
|
||||
}
|
||||
}
|
||||
}, 500 );
|
||||
});
|
||||
})( jQuery );",
|
||||
esc_js( $action ),
|
||||
esc_js( $title ),
|
||||
esc_js( $first_question ),
|
||||
esc_js( $second_question ),
|
||||
esc_js( $this->onsubmit_label )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current published product count.
|
||||
*
|
||||
* @return integer The current published product count.
|
||||
*/
|
||||
private function get_product_count() {
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'paginate' => true,
|
||||
'return' => 'ids',
|
||||
'status' => array( 'publish' ),
|
||||
)
|
||||
);
|
||||
$products = $query->get_products();
|
||||
$product_count = intval( $products->total );
|
||||
|
||||
return $product_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current shop order count.
|
||||
*
|
||||
* @return integer The current shop order count.
|
||||
*/
|
||||
private function get_shop_order_count() {
|
||||
$query = new \WC_Order_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'paginate' => true,
|
||||
'return' => 'ids',
|
||||
)
|
||||
);
|
||||
$shop_orders = $query->get_orders();
|
||||
$shop_order_count = intval( $shop_orders->total );
|
||||
|
||||
return $shop_order_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the action has already been shown.
|
||||
*
|
||||
* @param string $action The action to check.
|
||||
*
|
||||
* @return bool Whether the action has already been shown.
|
||||
*/
|
||||
private function has_been_shown( $action ) {
|
||||
$shown_for_features = get_option( self::SHOWN_FOR_ACTIONS_OPTION_NAME, array() );
|
||||
$has_been_shown = in_array( $action, $shown_for_features, true );
|
||||
|
||||
return $has_been_shown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the item to the CES tracks queue.
|
||||
*
|
||||
* @param array $item The item to enqueue.
|
||||
*/
|
||||
private function enqueue_to_ces_tracks( $item ) {
|
||||
$queue = get_option(
|
||||
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||
array()
|
||||
);
|
||||
|
||||
$has_duplicate = array_filter(
|
||||
$queue,
|
||||
function ( $queue_item ) use ( $item ) {
|
||||
return $queue_item['action'] === $item['action'];
|
||||
}
|
||||
);
|
||||
if ( $has_duplicate ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue[] = $item;
|
||||
|
||||
update_option(
|
||||
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||
$queue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey on using search dynamically.
|
||||
*
|
||||
* @param string $search_area Search area such as "product" or "shop_order".
|
||||
* @param string $page_now Value of window.pagenow.
|
||||
* @param string $admin_page Value of window.adminpage.
|
||||
*/
|
||||
public function enqueue_ces_survey_for_search( $search_area, $page_now, $admin_page ) {
|
||||
if ( $this->has_been_shown( self::SEARCH_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::SEARCH_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to use search?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The search feature in WooCommerce is easy to use.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The search\'s functionality meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => $page_now,
|
||||
'adminpage' => $admin_page,
|
||||
'props' => (object) array(
|
||||
'search_area' => $search_area,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the post status lifecycle, to detect relevant user actions
|
||||
* that we want to survey about.
|
||||
*
|
||||
* @param string $new_status The new status.
|
||||
* @param string $old_status The old status.
|
||||
* @param Post $post The post.
|
||||
*/
|
||||
public function run_on_transition_post_status(
|
||||
$new_status,
|
||||
$old_status,
|
||||
$post
|
||||
) {
|
||||
if ( 'product' === $post->post_type ) {
|
||||
$this->maybe_enqueue_ces_survey_for_product( $new_status, $old_status );
|
||||
} elseif ( 'shop_order' === $post->post_type ) {
|
||||
$this->enqueue_ces_survey_for_edited_shop_order();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe enqueue the CES survey, if product is being added or edited.
|
||||
*
|
||||
* @param string $new_status The new status.
|
||||
* @param string $old_status The old status.
|
||||
*/
|
||||
private function maybe_enqueue_ces_survey_for_product(
|
||||
$new_status,
|
||||
$old_status
|
||||
) {
|
||||
if ( 'publish' !== $new_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'publish' !== $old_status ) {
|
||||
$this->enqueue_ces_survey_for_new_product();
|
||||
} else {
|
||||
$this->enqueue_ces_survey_for_edited_product();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey trigger for a new product.
|
||||
*/
|
||||
private function enqueue_ces_survey_for_new_product() {
|
||||
if ( $this->has_been_shown( self::PRODUCT_ADD_PUBLISH_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::PRODUCT_ADD_PUBLISH_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to add a product?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The product creation screen is easy to use.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The product creation screen\'s functionality meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'product',
|
||||
'adminpage' => 'post-php',
|
||||
'props' => array(
|
||||
'product_count' => $this->get_product_count(),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey trigger for an existing product.
|
||||
*/
|
||||
private function enqueue_ces_survey_for_edited_product() {
|
||||
if ( $this->has_been_shown( self::PRODUCT_UPDATE_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::PRODUCT_UPDATE_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to edit your product?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The product update process is easy to complete.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The product update process meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'product',
|
||||
'adminpage' => 'post-php',
|
||||
'props' => array(
|
||||
'product_count' => $this->get_product_count(),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey trigger for an existing shop order.
|
||||
*/
|
||||
private function enqueue_ces_survey_for_edited_shop_order() {
|
||||
if ( $this->has_been_shown( self::SHOP_ORDER_UPDATE_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::SHOP_ORDER_UPDATE_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to update an order?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The order details screen is easy to use.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The order details screen\'s functionality meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'shop_order',
|
||||
'adminpage' => 'post-php',
|
||||
'props' => array(
|
||||
'order_count' => $this->get_shop_order_count(),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe clear the CES tracks queue, executed on every page load. If the
|
||||
* clear option is set it clears the queue. In practice, this executes a
|
||||
* page load after the queued CES tracks are displayed on the client, which
|
||||
* sets the clear option.
|
||||
*/
|
||||
public function maybe_clear_ces_tracks_queue() {
|
||||
$clear_ces_tracks_queue_for_page = get_option(
|
||||
self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME,
|
||||
false
|
||||
);
|
||||
|
||||
if ( ! $clear_ces_tracks_queue_for_page ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue = get_option(
|
||||
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||
array()
|
||||
);
|
||||
$remaining_items = array_filter(
|
||||
$queue,
|
||||
function ( $item ) use ( $clear_ces_tracks_queue_for_page ) {
|
||||
return $clear_ces_tracks_queue_for_page['pagenow'] !== $item['pagenow']
|
||||
|| $clear_ces_tracks_queue_for_page['adminpage'] !== $item['adminpage'];
|
||||
}
|
||||
);
|
||||
|
||||
update_option(
|
||||
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||
array_values( $remaining_items )
|
||||
);
|
||||
update_option( self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a script to footer to trigger CES on adding product categories.
|
||||
*/
|
||||
public function add_script_track_product_categories() {
|
||||
if ( $this->has_been_shown( self::ADD_PRODUCT_CATEGORIES_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wc_enqueue_js(
|
||||
$this->get_script_track_edit_php(
|
||||
self::ADD_PRODUCT_CATEGORIES_ACTION_NAME,
|
||||
__( 'How easy was it to add product category?', 'woocommerce' ),
|
||||
__( 'The product category details screen is easy to use.', 'woocommerce' ),
|
||||
__( "The product category details screen's functionality meets my needs.", 'woocommerce' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a script to footer to trigger CES on adding product tags.
|
||||
*/
|
||||
public function add_script_track_product_tags() {
|
||||
if ( $this->has_been_shown( self::ADD_PRODUCT_TAGS_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wc_enqueue_js(
|
||||
$this->get_script_track_edit_php(
|
||||
self::ADD_PRODUCT_TAGS_ACTION_NAME,
|
||||
__( 'How easy was it to add a product tag?', 'woocommerce' ),
|
||||
__( 'The product tag details screen is easy to use.', 'woocommerce' ),
|
||||
__( "The product tag details screen's functionality meets my needs.", 'woocommerce' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe enqueue the CES survey on product import, if step is done.
|
||||
*/
|
||||
public function run_on_product_import() {
|
||||
// We're only interested in when the importer completes.
|
||||
if ( empty( $_GET['step'] ) || 'done' !== $_GET['step'] ) { // phpcs:ignore CSRF ok.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->has_been_shown( self::IMPORT_PRODUCTS_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::IMPORT_PRODUCTS_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to import products?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The product import process is easy to complete.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The product import process meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'product_page_product_importer',
|
||||
'adminpage' => 'product_page_product_importer',
|
||||
'props' => (object) array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey trigger for setting changes.
|
||||
*/
|
||||
public function run_on_update_options() {
|
||||
// $current_tab is set when WC_Admin_Settings::save_settings is called.
|
||||
global $current_tab;
|
||||
global $current_section;
|
||||
|
||||
if ( $this->has_been_shown( self::SETTINGS_CHANGE_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$props = array(
|
||||
'settings_area' => $current_tab,
|
||||
);
|
||||
|
||||
if ( $current_section ) {
|
||||
$props['settings_section'] = $current_section;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::SETTINGS_CHANGE_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to update your settings?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'The settings screen is easy to use.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'The settings screen\'s functionality meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'woocommerce_page_wc-settings',
|
||||
'adminpage' => 'woocommerce_page_wc-settings',
|
||||
'props' => (object) $props,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the CES survey on adding new product attributes.
|
||||
*/
|
||||
public function run_on_add_product_attributes() {
|
||||
if ( $this->has_been_shown( self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_to_ces_tracks(
|
||||
array(
|
||||
'action' => self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME,
|
||||
'title' => __(
|
||||
'How easy was it to add a product attribute?',
|
||||
'woocommerce'
|
||||
),
|
||||
'firstQuestion' => __(
|
||||
'Product attributes are easy to use.',
|
||||
'woocommerce'
|
||||
),
|
||||
'secondQuestion' => __(
|
||||
'Product attributes\' functionality meets my needs.',
|
||||
'woocommerce'
|
||||
),
|
||||
'onsubmit_label' => $this->onsubmit_label,
|
||||
'pagenow' => 'product_page_product_attributes',
|
||||
'adminpage' => 'product_page_product_attributes',
|
||||
'props' => (object) array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine on initiating CES survey on searching for product or orders.
|
||||
*/
|
||||
public function run_on_load_edit_php() {
|
||||
$allowed_types = array( 'product', 'shop_order' );
|
||||
$post_type = get_current_screen()->post_type;
|
||||
|
||||
// We're only interested for certain post types.
|
||||
if ( ! in_array( $post_type, $allowed_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine whether request is search by "s" GET parameter.
|
||||
if ( empty( $_GET['s'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
return;
|
||||
}
|
||||
|
||||
$page_now = 'edit-' . $post_type;
|
||||
$this->enqueue_ces_survey_for_search( $post_type, $page_now, 'edit-php' );
|
||||
}
|
||||
}
|
||||
278
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Events.php
Normal file
278
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Events.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
/**
|
||||
* Handle cron events.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsDataSourcePoller;
|
||||
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\ManageOrdersOnTheGo;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MarketingJetpack;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MobileApp;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\NewSalesRecord;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\OnboardingPayments;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\OnlineClothingStore;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\PaymentsMoreInfoNeeded;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\PaymentsRemindMeLater;
|
||||
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\TrackingOptIn;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
|
||||
use Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler;
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller;
|
||||
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\RemoteFreeExtensionsDataSourcePoller;
|
||||
|
||||
/**
|
||||
* Events Class.
|
||||
*/
|
||||
class Events {
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __construct() {}
|
||||
|
||||
/**
|
||||
* Array of note class to be added or updated.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $note_classes_to_added_or_updated = array(
|
||||
AddFirstProduct::class,
|
||||
ChoosingTheme::class,
|
||||
CustomizeStoreWithBlocks::class,
|
||||
CustomizingProductCatalog::class,
|
||||
EditProductsOnTheMove::class,
|
||||
EUVATNumber::class,
|
||||
FirstProduct::class,
|
||||
LaunchChecklist::class,
|
||||
MagentoMigration::class,
|
||||
ManageOrdersOnTheGo::class,
|
||||
MarketingJetpack::class,
|
||||
MigrateFromShopify::class,
|
||||
MobileApp::class,
|
||||
NewSalesRecord::class,
|
||||
OnboardingPayments::class,
|
||||
OnlineClothingStore::class,
|
||||
PaymentsMoreInfoNeeded::class,
|
||||
PaymentsRemindMeLater::class,
|
||||
PerformanceOnMobile::class,
|
||||
PersonalizeStore::class,
|
||||
RealTimeOrderAlerts::class,
|
||||
TrackingOptIn::class,
|
||||
WooCommercePayments::class,
|
||||
WooCommerceSubscriptions::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* The other note classes that are added in other places.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $other_note_classes = array(
|
||||
CouponPageMoved::class,
|
||||
InstallJPAndWCSPlugins::class,
|
||||
OrderMilestones::class,
|
||||
SellingOnlineCourses::class,
|
||||
UnsecuredReportFiles::class,
|
||||
WooSubscriptionsNotes::class,
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*
|
||||
* @return object Instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( null === static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron event handlers.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'wc_admin_daily', array( $this, 'do_wc_admin_daily' ) );
|
||||
add_filter( 'woocommerce_get_note_from_db', array( $this, 'get_note_from_db' ), 10, 1 );
|
||||
|
||||
// Initialize the WC_Notes_Refund_Returns Note to attach hook.
|
||||
\WC_Notes_Refund_Returns::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily events to run.
|
||||
*
|
||||
* Note: Order_Milestones::possibly_add_note is hooked to this as well.
|
||||
*/
|
||||
public function do_wc_admin_daily() {
|
||||
$this->possibly_add_notes();
|
||||
$this->possibly_delete_notes();
|
||||
$this->possibly_update_notes();
|
||||
$this->possibly_refresh_data_source_pollers();
|
||||
|
||||
if ( $this->is_remote_inbox_notifications_enabled() ) {
|
||||
RemoteInboxNotificationsDataSourcePoller::get_instance()->read_specs_from_data_sources();
|
||||
RemoteInboxNotificationsEngine::run();
|
||||
}
|
||||
|
||||
if ( $this->is_merchant_email_notifications_enabled() ) {
|
||||
MerchantEmailNotifications::run();
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'core-profiler' ) ) {
|
||||
( new MailchimpScheduler() )->run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get note.
|
||||
*
|
||||
* @param Note $note_from_db The note object from the database.
|
||||
*/
|
||||
public function get_note_from_db( $note_from_db ) {
|
||||
if ( ! $note_from_db instanceof Note || get_user_locale() === $note_from_db->get_locale() ) {
|
||||
return $note_from_db;
|
||||
}
|
||||
|
||||
$note_classes = array_merge( self::$note_classes_to_added_or_updated, self::$other_note_classes );
|
||||
foreach ( $note_classes as $note_class ) {
|
||||
if ( defined( "$note_class::NOTE_NAME" ) && $note_class::NOTE_NAME === $note_from_db->get_name() ) {
|
||||
$note_from_class = method_exists( $note_class, 'get_note' ) ? $note_class::get_note() : null;
|
||||
|
||||
if ( $note_from_class instanceof Note ) {
|
||||
$note = clone $note_from_db;
|
||||
$note->set_title( $note_from_class->get_title() );
|
||||
$note->set_content( $note_from_class->get_content() );
|
||||
$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;
|
||||
}
|
||||
}
|
||||
return $note_from_db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds notes that should be added.
|
||||
*/
|
||||
protected function possibly_add_notes() {
|
||||
foreach ( self::$note_classes_to_added_or_updated as $note_class ) {
|
||||
if ( method_exists( $note_class, 'possibly_add_note' ) ) {
|
||||
$note_class::possibly_add_note();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes notes that should be deleted.
|
||||
*/
|
||||
protected function possibly_delete_notes() {
|
||||
PaymentsRemindMeLater::delete_if_not_applicable();
|
||||
PaymentsMoreInfoNeeded::delete_if_not_applicable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates notes that should be updated.
|
||||
*/
|
||||
protected function possibly_update_notes() {
|
||||
foreach ( self::$note_classes_to_added_or_updated as $note_class ) {
|
||||
if ( method_exists( $note_class, 'possibly_update_note' ) ) {
|
||||
$note_class::possibly_update_note();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if remote inbox notifications are enabled.
|
||||
*
|
||||
* @return bool Whether remote inbox notifications are enabled.
|
||||
*/
|
||||
protected function is_remote_inbox_notifications_enabled() {
|
||||
// Check if the feature flag is disabled.
|
||||
if ( ! Features::is_enabled( 'remote-inbox-notifications' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the site has opted out of marketplace suggestions.
|
||||
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) !== 'yes' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All checks have passed.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if merchant email notifications are enabled.
|
||||
*
|
||||
* @return bool Whether merchant email notifications are enabled.
|
||||
*/
|
||||
protected function is_merchant_email_notifications_enabled() {
|
||||
// Check if the feature flag is disabled.
|
||||
if ( get_option( 'woocommerce_merchant_email_notifications', 'no' ) !== 'yes' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All checks have passed.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh transient for the following DataSourcePollers on wc_admin_daily cron job.
|
||||
* - PaymentGatewaySuggestionsDataSourcePoller
|
||||
* - RemoteFreeExtensionsDataSourcePoller
|
||||
*/
|
||||
protected function possibly_refresh_data_source_pollers() {
|
||||
$completed_tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
|
||||
|
||||
if ( ! in_array( 'payments', $completed_tasks, true ) && ! in_array( 'woocommerce-payments', $completed_tasks, true ) ) {
|
||||
PaymentGatewaySuggestionsDataSourcePoller::get_instance()->read_specs_from_data_sources();
|
||||
}
|
||||
|
||||
if ( ! in_array( 'store_details', $completed_tasks, true ) && ! in_array( 'marketing', $completed_tasks, true ) ) {
|
||||
RemoteFreeExtensionsDataSourcePoller::get_instance()->read_specs_from_data_sources();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Feature plugin main class.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\API;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones;
|
||||
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\SellingOnlineCourses;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Admin\PluginsInstaller;
|
||||
use Automattic\WooCommerce\Admin\ReportExporter;
|
||||
use Automattic\WooCommerce\Admin\ReportsSync;
|
||||
use Automattic\WooCommerce\Internal\Admin\CategoryLookup;
|
||||
use Automattic\WooCommerce\Internal\Admin\Events;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\Onboarding;
|
||||
|
||||
/**
|
||||
* Feature plugin main class.
|
||||
*
|
||||
* @internal This file will not be bundled with woo core, only the feature plugin.
|
||||
* @internal Note this is not called WC_Admin due to a class already existing in core with that name.
|
||||
*/
|
||||
class FeaturePlugin {
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __construct() {}
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*
|
||||
* @return object Instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( null === static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the feature plugin, only if we can detect both Gutenberg and WooCommerce.
|
||||
*/
|
||||
public function init() {
|
||||
// Bail if WC isn't initialized (This can be called from WCAdmin's entrypoint).
|
||||
if ( ! defined( 'WC_ABSPATH' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the page controller functions file first to prevent fatal errors when disabling WooCommerce Admin.
|
||||
$this->define_constants();
|
||||
require_once WC_ADMIN_ABSPATH . '/includes/react-admin/page-controller-functions.php';
|
||||
require_once WC_ADMIN_ABSPATH . '/src/Admin/Notes/DeprecatedNotes.php';
|
||||
require_once WC_ADMIN_ABSPATH . '/includes/react-admin/core-functions.php';
|
||||
require_once WC_ADMIN_ABSPATH . '/includes/react-admin/feature-config.php';
|
||||
require_once WC_ADMIN_ABSPATH . '/includes/react-admin/wc-admin-update-functions.php';
|
||||
require_once WC_ADMIN_ABSPATH . '/includes/react-admin/class-experimental-abtest.php';
|
||||
|
||||
if ( did_action( 'plugins_loaded' ) ) {
|
||||
self::on_plugins_loaded();
|
||||
} else {
|
||||
// Make sure we hook into `plugins_loaded` before core's Automattic\WooCommerce\Package::init().
|
||||
// If core is network activated but we aren't, the packaged version of WooCommerce Admin will
|
||||
// attempt to use a data store that hasn't been loaded yet - because we've defined our constants here.
|
||||
// See: https://github.com/woocommerce/woocommerce-admin/issues/3869.
|
||||
add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), 9 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup plugin once all other plugins are loaded.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_plugins_loaded() {
|
||||
$this->hooks();
|
||||
$this->includes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Constants.
|
||||
*/
|
||||
protected function define_constants() {
|
||||
$this->define( 'WC_ADMIN_APP', 'wc-admin-app' );
|
||||
$this->define( 'WC_ADMIN_ABSPATH', WC_ABSPATH );
|
||||
$this->define( 'WC_ADMIN_DIST_JS_FOLDER', 'assets/client/admin/' );
|
||||
$this->define( 'WC_ADMIN_DIST_CSS_FOLDER', 'assets/client/admin/' );
|
||||
$this->define( 'WC_ADMIN_PLUGIN_FILE', WC_PLUGIN_FILE );
|
||||
|
||||
/**
|
||||
* Define the WC Admin Images Folder URL.
|
||||
*
|
||||
* @deprecated 6.7.0
|
||||
* @var string
|
||||
*/
|
||||
if ( ! defined( 'WC_ADMIN_IMAGES_FOLDER_URL' ) ) {
|
||||
/**
|
||||
* Define the WC Admin Images Folder URL.
|
||||
*
|
||||
* @deprecated 6.7.0
|
||||
* @var string
|
||||
*/
|
||||
define( 'WC_ADMIN_IMAGES_FOLDER_URL', plugins_url( 'assets/images', WC_PLUGIN_FILE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the current WC Admin version.
|
||||
*
|
||||
* @deprecated 6.4.0
|
||||
* @var string
|
||||
*/
|
||||
if ( ! defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
|
||||
/**
|
||||
* Define the current WC Admin version.
|
||||
*
|
||||
* @deprecated 6.4.0
|
||||
* @var string
|
||||
*/
|
||||
define( 'WC_ADMIN_VERSION_NUMBER', '3.3.0' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Include WC Admin classes.
|
||||
*/
|
||||
public function includes() {
|
||||
// Initialize Database updates, option migrations, and Notes.
|
||||
Events::instance()->init();
|
||||
Notes::init();
|
||||
|
||||
// Initialize Plugins Installer.
|
||||
PluginsInstaller::init();
|
||||
PluginsHelper::init();
|
||||
|
||||
// Initialize API.
|
||||
API\Init::instance();
|
||||
|
||||
if ( Features::is_enabled( 'onboarding' ) ) {
|
||||
Onboarding::init();
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'analytics' ) ) {
|
||||
// Initialize Reports syncing.
|
||||
ReportsSync::init();
|
||||
CategoryLookup::instance()->init();
|
||||
|
||||
// Initialize Reports exporter.
|
||||
ReportExporter::init();
|
||||
}
|
||||
|
||||
// Admin note providers.
|
||||
// @todo These should be bundled in the features/ folder, but loading them from there currently has a load order issue.
|
||||
new WooSubscriptionsNotes();
|
||||
new OrderMilestones();
|
||||
new TrackingOptIn();
|
||||
new WooCommercePayments();
|
||||
new InstallJPAndWCSPlugins();
|
||||
new SellingOnlineCourses();
|
||||
new MagentoMigration();
|
||||
|
||||
// Initialize MerchantEmailNotifications.
|
||||
MerchantEmailNotifications::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up our admin hooks and plugin loader.
|
||||
*/
|
||||
protected function hooks() {
|
||||
add_filter( 'woocommerce_admin_features', array( $this, 'replace_supported_features' ), 0 );
|
||||
|
||||
Loader::get_instance();
|
||||
WCAdminAssets::get_instance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overwrites the allowed features array using a local `feature-config.php` file.
|
||||
*
|
||||
* @param array $features Array of feature slugs.
|
||||
*/
|
||||
public function replace_supported_features( $features ) {
|
||||
/**
|
||||
* Get additional feature config
|
||||
*
|
||||
* @since 6.5.0
|
||||
*/
|
||||
$feature_config = apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() );
|
||||
$features = array_keys( array_filter( $feature_config ) );
|
||||
return $features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define constant if not already set.
|
||||
*
|
||||
* @param string $name Constant name.
|
||||
* @param string|bool $value Constant value.
|
||||
*/
|
||||
protected function define( $name, $value ) {
|
||||
if ( ! defined( $name ) ) {
|
||||
define( $name, $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent cloning.
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* Prevent unserializing.
|
||||
*/
|
||||
public function __wakeup() {
|
||||
die();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Homescreen.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Shipping;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the homescreen feature.
|
||||
*/
|
||||
class Homescreen {
|
||||
/**
|
||||
* Menu slug.
|
||||
*/
|
||||
const MENU_SLUG = 'wc-admin';
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Homescreen instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
add_action( 'admin_menu', array( $this, 'register_page' ) );
|
||||
// In WC Core 5.1 $submenu manipulation occurs in admin_menu, not admin_head. See https://github.com/woocommerce/woocommerce/pull/29088.
|
||||
if ( version_compare( WC_VERSION, '5.1', '>=' ) ) {
|
||||
// priority is 20 to run after admin_menu hook for woocommerce runs, so that submenu is populated.
|
||||
add_action( 'admin_menu', array( $this, 'possibly_remove_woocommerce_menu' ) );
|
||||
add_action( 'admin_menu', array( $this, 'update_link_structure' ), 20 );
|
||||
} else {
|
||||
// priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165.
|
||||
add_action( 'admin_head', array( $this, 'update_link_structure' ), 20 );
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );
|
||||
|
||||
if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
|
||||
add_filter(
|
||||
'woocommerce_admin_shared_settings',
|
||||
array( $this, 'maybe_set_default_shipping_options_on_home' ),
|
||||
9999
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set free shipping in the same country as the store default
|
||||
* Flag rate in all other countries when any of the following conditions are ture
|
||||
*
|
||||
* - The store sells physical products, has JP and WCS installed and connected, and is located in the US.
|
||||
* - The store sells physical products, and is not located in US/Canada/Australia/UK (irrelevant if JP is installed or not).
|
||||
* - The store sells physical products and is located in US, but JP and WCS are not installed.
|
||||
*
|
||||
* @param array $settings shared admin settings.
|
||||
* @return array
|
||||
*/
|
||||
public function maybe_set_default_shipping_options_on_home( $settings ) {
|
||||
if ( ! function_exists( 'get_current_screen' ) ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$current_screen = get_current_screen();
|
||||
|
||||
// Abort if it's not the homescreen.
|
||||
if ( ! isset( $current_screen->id ) || 'woocommerce_page_wc-admin' !== $current_screen->id ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Abort if we already created the shipping options.
|
||||
$already_created = get_option( 'woocommerce_admin_created_default_shipping_zones' );
|
||||
if ( $already_created === 'yes' ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$zone_count = count( \WC_Data_Store::load( 'shipping-zone' )->get_zones() );
|
||||
if ( $zone_count ) {
|
||||
update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' );
|
||||
update_option( 'woocommerce_admin_reviewed_default_shipping_zones', 'yes' );
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$user_skipped_obw = $settings['onboarding']['profile']['skipped'] ?? false;
|
||||
$store_address = $settings['preloadSettings']['general']['woocommerce_store_address'] ?? '';
|
||||
$product_types = $settings['onboarding']['profile']['product_types'] ?? array();
|
||||
$user_has_set_store_country = $settings['onboarding']['profile']['is_store_country_set'] ?? false;
|
||||
|
||||
// Do not proceed if user has not filled out their country in the onboarding profiler.
|
||||
if ( ! $user_has_set_store_country ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// If user skipped the obw or has not completed the store_details
|
||||
// then we assume the user is going to sell physical products.
|
||||
if ( $user_skipped_obw || '' === $store_address ) {
|
||||
$product_types[] = 'physical';
|
||||
}
|
||||
|
||||
if ( false === in_array( 'physical', $product_types, true ) ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$country_code = wc_format_country_state_string( $settings['preloadSettings']['general']['woocommerce_default_country'] )['country'];
|
||||
$country_name = WC()->countries->get_countries()[ $country_code ] ?? null;
|
||||
|
||||
$is_jetpack_installed = in_array( 'jetpack', $settings['plugins']['installedPlugins'] ?? array(), true );
|
||||
$is_wcs_installed = in_array( 'woocommerce-services', $settings['plugins']['installedPlugins'] ?? array(), true );
|
||||
|
||||
if (
|
||||
( 'US' === $country_code && $is_jetpack_installed )
|
||||
||
|
||||
( ! in_array( $country_code, array( 'CA', 'AU', 'NZ', 'SG', 'HK', 'GB', 'ES', 'IT', 'DE', 'FR', 'CL', 'AR', 'PE', 'BR', 'UY', 'GT', 'NL', 'AT', 'BE' ), true ) )
|
||||
||
|
||||
( 'US' === $country_code && false === $is_jetpack_installed && false === $is_wcs_installed )
|
||||
) {
|
||||
$zone = new \WC_Shipping_Zone();
|
||||
$zone->set_zone_name( $country_name );
|
||||
$zone->add_location( $country_code, 'country' );
|
||||
|
||||
// Method creation has no default title, use the REST API to add a title.
|
||||
$instance_id = $zone->add_shipping_method( 'free_shipping' );
|
||||
$request = new \WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'settings' => array(
|
||||
'title' => 'Free shipping',
|
||||
),
|
||||
)
|
||||
);
|
||||
rest_do_request( $request );
|
||||
|
||||
update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' );
|
||||
Shipping::delete_zone_count_transient();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store performance indicators, row settings, and chart type settings for users.
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
*/
|
||||
public function add_user_data_fields( $user_data_fields ) {
|
||||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'homepage_layout',
|
||||
'homepage_stats',
|
||||
'task_list_tracked_started_tasks',
|
||||
'help_panel_highlight_shown',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers home page.
|
||||
*/
|
||||
public function register_page() {
|
||||
// Register a top-level item for users who cannot view the core WooCommerce menu.
|
||||
if ( ! self::is_admin_user() ) {
|
||||
wc_admin_register_page(
|
||||
array(
|
||||
'id' => 'woocommerce-home',
|
||||
'title' => __( 'WooCommerce', 'woocommerce' ),
|
||||
'path' => self::MENU_SLUG,
|
||||
'capability' => 'read',
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
wc_admin_register_page(
|
||||
array(
|
||||
'id' => 'woocommerce-home',
|
||||
'title' => __( 'Home', 'woocommerce' ),
|
||||
'parent' => 'woocommerce',
|
||||
'path' => self::MENU_SLUG,
|
||||
'order' => 0,
|
||||
'capability' => 'read',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user can access the top-level WooCommerce item.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_admin_user() {
|
||||
if ( ! class_exists( 'WC_Admin_Menus', false ) ) {
|
||||
include_once WC_ABSPATH . 'includes/admin/class-wc-admin-menus.php';
|
||||
}
|
||||
if ( method_exists( 'WC_Admin_Menus', 'can_view_woocommerce_menu_item' ) ) {
|
||||
return \WC_Admin_Menus::can_view_woocommerce_menu_item() || current_user_can( 'manage_woocommerce' );
|
||||
} else {
|
||||
// We leave this line for WC versions <= 6.2.
|
||||
return current_user_can( 'edit_others_shop_orders' ) || current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly remove the WooCommerce menu item if it was purely used to access wc-admin pages.
|
||||
*/
|
||||
public function possibly_remove_woocommerce_menu() {
|
||||
global $menu;
|
||||
|
||||
if ( self::is_admin_user() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $menu as $key => $menu_item ) {
|
||||
if ( self::MENU_SLUG !== $menu_item[2] || 'read' !== $menu_item[1] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset( $menu[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the WooCommerce menu structure to make our main dashboard/handler
|
||||
* the top level link for 'WooCommerce'.
|
||||
*/
|
||||
public function update_link_structure() {
|
||||
global $submenu;
|
||||
// User does not have capabilites to see the submenu.
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) || empty( $submenu['woocommerce'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wc_admin_key = null;
|
||||
foreach ( $submenu['woocommerce'] as $submenu_key => $submenu_item ) {
|
||||
if ( self::MENU_SLUG === $submenu_item[2] ) {
|
||||
$wc_admin_key = $submenu_key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $wc_admin_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $submenu['woocommerce'][ $wc_admin_key ];
|
||||
|
||||
// Move menu item to top of array.
|
||||
unset( $submenu['woocommerce'][ $wc_admin_key ] );
|
||||
array_unshift( $submenu['woocommerce'], $menu );
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload options to prime state of the application.
|
||||
*
|
||||
* @param array $options Array of options to preload.
|
||||
* @return array
|
||||
*/
|
||||
public function preload_options( $options ) {
|
||||
$options[] = 'woocommerce_default_homepage_layout';
|
||||
$options[] = 'woocommerce_admin_install_timestamp';
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
552
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Loader.php
Normal file
552
wp/wp-content/plugins/woocommerce/src/Internal/Admin/Loader.php
Normal file
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
/**
|
||||
* Register the scripts, styles, and includes needed for pieces of the WooCommerce Admin experience.
|
||||
*/
|
||||
|
||||
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\ProductReviews\Reviews;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
|
||||
use Automattic\WooCommerce\Internal\Admin\Settings;
|
||||
|
||||
/**
|
||||
* Loader Class.
|
||||
*/
|
||||
class Loader {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Loader instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* An array of classes to load from the includes folder.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $classes = array();
|
||||
|
||||
/**
|
||||
* WordPress capability required to use analytics features.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $required_capability = null;
|
||||
|
||||
/**
|
||||
* An array of dependencies that have been preloaded (to avoid duplicates).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $preloaded_dependencies = array(
|
||||
'script' => array(),
|
||||
'style' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Hooks added here should be removed in `wc_admin_initialize` via the feature plugin.
|
||||
*/
|
||||
public function __construct() {
|
||||
Features::get_instance();
|
||||
WCAdminSharedSettings::get_instance();
|
||||
Translations::get_instance();
|
||||
WCAdminUser::get_instance();
|
||||
Settings::get_instance();
|
||||
SiteHealth::get_instance();
|
||||
SystemStatusReport::get_instance();
|
||||
|
||||
wc_get_container()->get( Reviews::class );
|
||||
wc_get_container()->get( ReviewsCommentsOverrides::class );
|
||||
|
||||
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
|
||||
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
|
||||
add_action( 'in_admin_header', array( __CLASS__, 'embed_page_header' ) );
|
||||
add_action( 'admin_head', array( __CLASS__, 'remove_notices' ) );
|
||||
add_action( 'admin_head', array( __CLASS__, 'smart_app_banner' ) );
|
||||
add_action( 'admin_notices', array( __CLASS__, 'inject_before_notices' ), -9999 );
|
||||
add_action( 'admin_notices', array( __CLASS__, 'inject_after_notices' ), PHP_INT_MAX );
|
||||
|
||||
// Added this hook to delete the field woocommerce_onboarding_homepage_post_id when deleting the homepage.
|
||||
add_action( 'trashed_post', array( __CLASS__, 'delete_homepage' ) );
|
||||
|
||||
/*
|
||||
* Remove the emoji script as it always defaults to replacing emojis with Twemoji images.
|
||||
* Gutenberg has also disabled emojis. More on that here -> https://github.com/WordPress/gutenberg/pull/6151
|
||||
*/
|
||||
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
|
||||
|
||||
add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns breadcrumbs for the current page.
|
||||
*/
|
||||
private static function get_embed_breadcrumbs() {
|
||||
return wc_admin_get_breadcrumbs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs breadcrumbs via PHP for the initial load of an embedded page.
|
||||
*
|
||||
* @param array $section Section to create breadcrumb from.
|
||||
*/
|
||||
private static function output_heading( $section ) {
|
||||
echo esc_html( $section );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a div for the header embed to render into.
|
||||
* The initial contents here are meant as a place loader for when the PHP page initialy loads.
|
||||
*/
|
||||
public static function embed_page_header() {
|
||||
if ( ! PageController::is_admin_page() && ! PageController::is_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! PageController::is_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sections = self::get_embed_breadcrumbs();
|
||||
$sections = is_array( $sections ) ? $sections : array( $sections );
|
||||
?>
|
||||
<div id="woocommerce-embedded-root" class="is-embed-loading">
|
||||
<div class="woocommerce-layout">
|
||||
<div class="woocommerce-layout__header is-embed-loading">
|
||||
<h1 class="woocommerce-layout__header-heading">
|
||||
<?php self::output_heading( end( $sections ) ); ?>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
|
||||
*
|
||||
* @param string $admin_body_class Body class to add.
|
||||
*/
|
||||
public static function add_admin_body_classes( $admin_body_class = '' ) {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return $admin_body_class;
|
||||
}
|
||||
|
||||
$classes = explode( ' ', trim( $admin_body_class ) );
|
||||
$classes[] = 'woocommerce-admin-page';
|
||||
if ( PageController::is_embed_page() ) {
|
||||
$classes[] = 'woocommerce-embed-page';
|
||||
}
|
||||
|
||||
/**
|
||||
* Some routes or features like onboarding hide the wp-admin navigation and masterbar.
|
||||
* Setting `woocommerce_admin_is_loading` to true allows us to premeptively hide these
|
||||
* elements while the JS app loads.
|
||||
* This class needs to be removed by those feature components (like <ProfileWizard />).
|
||||
*
|
||||
* @param bool $is_loading If WooCommerce Admin is loading a fullscreen view.
|
||||
*/
|
||||
$is_loading = apply_filters( 'woocommerce_admin_is_loading', false );
|
||||
|
||||
if ( PageController::is_admin_page() && $is_loading ) {
|
||||
$classes[] = 'woocommerce-admin-is-loading';
|
||||
}
|
||||
|
||||
$admin_body_class = implode( ' ', array_unique( $classes ) );
|
||||
return " $admin_body_class ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an iOS "Smart App Banner" for display on iOS Safari.
|
||||
* See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
||||
*/
|
||||
public static function smart_app_banner() {
|
||||
if ( PageController::is_admin_or_embed_page() ) {
|
||||
echo "
|
||||
<meta name='apple-itunes-app' content='app-id=1389130815'>
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes notices that should not be displayed on WC Admin pages.
|
||||
*/
|
||||
public static function remove_notices() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hello Dolly.
|
||||
if ( function_exists( 'hello_dolly' ) ) {
|
||||
remove_action( 'admin_notices', 'hello_dolly' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs before admin notices action and hides them.
|
||||
*/
|
||||
public static function inject_before_notices() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The JITMs won't be shown in the Onboarding Wizard.
|
||||
$is_onboarding = isset( $_GET['path'] ) && '/setup-wizard' === wc_clean( wp_unslash( $_GET['path'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$maybe_hide_jitm = $is_onboarding ? '-hide' : '';
|
||||
|
||||
echo '<div class="woocommerce-layout__jitm' . sanitize_html_class( $maybe_hide_jitm ) . '" id="jp-admin-notices"></div>';
|
||||
|
||||
// Wrap the notices in a hidden div to prevent flickering before
|
||||
// they are moved elsewhere in the page by WordPress Core.
|
||||
echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">';
|
||||
|
||||
if ( PageController::is_admin_page() ) {
|
||||
// Capture all notices and hide them. WordPress Core looks for
|
||||
// `.wp-header-end` and appends notices after it if found.
|
||||
// https://github.com/WordPress/WordPress/blob/f6a37e7d39e2534d05b9e542045174498edfe536/wp-admin/js/common.js#L737 .
|
||||
echo '<div class="wp-header-end" id="woocommerce-layout__notice-catcher"></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after admin notices and closes div.
|
||||
*/
|
||||
public static function inject_after_notices() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the hidden div used to prevent notices from flickering before
|
||||
// they are inserted elsewhere in the page.
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits Admin title based on section of wc-admin.
|
||||
*
|
||||
* @param string $admin_title Modifies admin title.
|
||||
* @todo Can we do some URL rewriting so we can figure out which page they are on server side?
|
||||
*/
|
||||
public static function update_admin_title( $admin_title ) {
|
||||
if (
|
||||
! did_action( 'current_screen' ) ||
|
||||
! PageController::is_admin_page()
|
||||
) {
|
||||
return $admin_title;
|
||||
}
|
||||
|
||||
$sections = self::get_embed_breadcrumbs();
|
||||
$pieces = array();
|
||||
|
||||
foreach ( $sections as $section ) {
|
||||
$pieces[] = is_array( $section ) ? $section[1] : $section;
|
||||
}
|
||||
|
||||
$pieces = array_reverse( $pieces );
|
||||
$title = implode( ' ‹ ', $pieces );
|
||||
|
||||
/* translators: %1$s: updated title, %2$s: blog info name */
|
||||
return sprintf( __( '%1$s ‹ %2$s', 'woocommerce' ), $title, get_bloginfo( 'name' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a div for the app to render into.
|
||||
*/
|
||||
public static function page_wrapper() {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<div id="root"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function add_component_settings( $settings ) {
|
||||
if ( ! is_admin() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wc_blocks_container' ) ) {
|
||||
global $wp_locale;
|
||||
// inject data not available via older versions of wc_blocks/woo.
|
||||
$settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() );
|
||||
$settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() );
|
||||
$settings['currency'] = self::get_currency_settings();
|
||||
$settings['locale'] = [
|
||||
'siteLocale' => isset( $settings['siteLocale'] )
|
||||
? $settings['siteLocale']
|
||||
: get_locale(),
|
||||
'userLocale' => isset( $settings['l10n']['userLocale'] )
|
||||
? $settings['l10n']['userLocale']
|
||||
: get_user_locale(),
|
||||
'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] )
|
||||
? $settings['l10n']['weekdaysShort']
|
||||
: array_values( $wp_locale->weekday_abbrev ),
|
||||
];
|
||||
}
|
||||
|
||||
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
|
||||
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
|
||||
if ( ! empty( $preload_data_endpoints ) ) {
|
||||
$preload_data = array_reduce(
|
||||
array_values( $preload_data_endpoints ),
|
||||
'rest_preload_api_request'
|
||||
);
|
||||
}
|
||||
|
||||
$preload_options = apply_filters( 'woocommerce_admin_preload_options', array() );
|
||||
if ( ! empty( $preload_options ) ) {
|
||||
foreach ( $preload_options as $option ) {
|
||||
$settings['preloadOptions'][ $option ] = get_option( $option );
|
||||
}
|
||||
}
|
||||
|
||||
$preload_settings = apply_filters( 'woocommerce_admin_preload_settings', array() );
|
||||
if ( ! empty( $preload_settings ) ) {
|
||||
$setting_options = new \WC_REST_Setting_Options_V2_Controller();
|
||||
foreach ( $preload_settings as $group ) {
|
||||
$group_settings = $setting_options->get_group_settings( $group );
|
||||
$preload_settings = [];
|
||||
foreach ( $group_settings as $option ) {
|
||||
if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) {
|
||||
$preload_settings[ $option['id'] ] = $option['value'];
|
||||
}
|
||||
}
|
||||
$settings['preloadSettings'][ $group ] = $preload_settings;
|
||||
}
|
||||
}
|
||||
|
||||
$user_controller = new \WP_REST_Users_Controller();
|
||||
$request = new \WP_REST_Request();
|
||||
$request->set_query_params( array( 'context' => 'edit' ) );
|
||||
$user_response = $user_controller->get_current_item( $request );
|
||||
$current_user_data = is_wp_error( $user_response ) ? (object) array() : $user_response->get_data();
|
||||
|
||||
$settings['currentUserData'] = $current_user_data;
|
||||
$settings['reviewsEnabled'] = get_option( 'woocommerce_enable_reviews' );
|
||||
$settings['manageStock'] = get_option( 'woocommerce_manage_stock' );
|
||||
$settings['commentModeration'] = get_option( 'comment_moderation' );
|
||||
$settings['notifyLowStockAmount'] = get_option( 'woocommerce_notify_low_stock_amount' );
|
||||
// @todo On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired,
|
||||
// and `wcAssetUrl` can be used in its place throughout the codebase.
|
||||
$settings['wcAdminAssetUrl'] = WC_ADMIN_IMAGES_FOLDER_URL;
|
||||
$settings['wcVersion'] = WC_VERSION;
|
||||
$settings['siteUrl'] = site_url();
|
||||
$settings['shopUrl'] = get_permalink( wc_get_page_id( 'shop' ) );
|
||||
$settings['homeUrl'] = home_url();
|
||||
$settings['dateFormat'] = get_option( 'date_format' );
|
||||
$settings['timeZone'] = wc_timezone_string();
|
||||
$settings['plugins'] = array(
|
||||
'installedPlugins' => PluginsHelper::get_installed_plugin_slugs(),
|
||||
'activePlugins' => Plugins::get_active_plugins(),
|
||||
);
|
||||
// Plugins that depend on changing the translation work on the server but not the client -
|
||||
// WooCommerce Branding is an example of this - so pass through the translation of
|
||||
// 'WooCommerce' to wcSettings.
|
||||
$settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce' );
|
||||
// We may have synced orders with a now-unregistered status.
|
||||
// E.g An extension that added statuses is now inactive or removed.
|
||||
$settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses();
|
||||
// The separator used for attributes found in Variation titles.
|
||||
$settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() );
|
||||
|
||||
if ( ! empty( $preload_data_endpoints ) ) {
|
||||
$settings['dataEndpoints'] = isset( $settings['dataEndpoints'] )
|
||||
? $settings['dataEndpoints']
|
||||
: [];
|
||||
foreach ( $preload_data_endpoints as $key => $endpoint ) {
|
||||
// Handle error case: rest_do_request() doesn't guarantee success.
|
||||
if ( empty( $preload_data[ $endpoint ] ) ) {
|
||||
$settings['dataEndpoints'][ $key ] = array();
|
||||
} else {
|
||||
$settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$settings = self::get_custom_settings( $settings );
|
||||
if ( PageController::is_embed_page() ) {
|
||||
$settings['embedBreadcrumbs'] = self::get_embed_breadcrumbs();
|
||||
}
|
||||
|
||||
$settings['allowMarketplaceSuggestions'] = WC_Marketplace_Suggestions::allow_suggestions();
|
||||
$settings['connectNonce'] = wp_create_nonce( 'connect' );
|
||||
$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format order statuses by removing a leading 'wc-' if present.
|
||||
*
|
||||
* @param array $statuses Order statuses.
|
||||
* @return array formatted statuses.
|
||||
*/
|
||||
public static function get_order_statuses( $statuses ) {
|
||||
$formatted_statuses = array();
|
||||
foreach ( $statuses as $key => $value ) {
|
||||
$formatted_key = preg_replace( '/^wc-/', '', $key );
|
||||
$formatted_statuses[ $formatted_key ] = $value;
|
||||
}
|
||||
return $formatted_statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all order statuses present in analytics tables that aren't registered.
|
||||
*
|
||||
* @return array Unregistered order statuses.
|
||||
*/
|
||||
public static function get_unregistered_order_statuses() {
|
||||
$registered_statuses = wc_get_order_statuses();
|
||||
$all_synced_statuses = OrdersDataStore::get_all_statuses();
|
||||
$unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) );
|
||||
$formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) );
|
||||
$formatted_statuses = array_keys( $formatted_status_keys );
|
||||
|
||||
return array_combine( $formatted_statuses, $formatted_statuses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the admin settings for use in the WC REST API
|
||||
*
|
||||
* @param array $groups Array of setting groups.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_settings_group( $groups ) {
|
||||
$groups[] = array(
|
||||
'id' => 'wc_admin',
|
||||
'label' => __( 'WooCommerce Admin', 'woocommerce' ),
|
||||
'description' => __( 'Settings for WooCommerce admin reporting.', 'woocommerce' ),
|
||||
);
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WC Admin specific settings
|
||||
*
|
||||
* @param array $settings Array of settings in wc admin group.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
$unregistered_statuses = self::get_unregistered_order_statuses();
|
||||
$registered_statuses = self::get_order_statuses( wc_get_order_statuses() );
|
||||
$all_statuses = array_merge( $unregistered_statuses, $registered_statuses );
|
||||
|
||||
$settings[] = array(
|
||||
'id' => 'woocommerce_excluded_report_order_statuses',
|
||||
'option_key' => 'woocommerce_excluded_report_order_statuses',
|
||||
'label' => __( 'Excluded report order statuses', 'woocommerce' ),
|
||||
'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce' ),
|
||||
'default' => array( 'pending', 'cancelled', 'failed' ),
|
||||
'type' => 'multiselect',
|
||||
'options' => $all_statuses,
|
||||
);
|
||||
$settings[] = array(
|
||||
'id' => 'woocommerce_actionable_order_statuses',
|
||||
'option_key' => 'woocommerce_actionable_order_statuses',
|
||||
'label' => __( 'Actionable order statuses', 'woocommerce' ),
|
||||
'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce' ),
|
||||
'default' => array( 'processing', 'on-hold' ),
|
||||
'type' => 'multiselect',
|
||||
'options' => $all_statuses,
|
||||
);
|
||||
$settings[] = array(
|
||||
'id' => 'woocommerce_default_date_range',
|
||||
'option_key' => 'woocommerce_default_date_range',
|
||||
'label' => __( 'Default Date Range', 'woocommerce' ),
|
||||
'description' => __( 'Default Date Range', 'woocommerce' ),
|
||||
'default' => 'period=month&compare=previous_year',
|
||||
'type' => 'text',
|
||||
);
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets custom settings used for WC Admin.
|
||||
*
|
||||
* @param array $settings Array of settings to merge into.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_custom_settings( $settings ) {
|
||||
$wc_rest_settings_options_controller = new \WC_REST_Setting_Options_Controller();
|
||||
$wc_admin_group_settings = $wc_rest_settings_options_controller->get_group_settings( 'wc_admin' );
|
||||
$settings['wcAdminSettings'] = array();
|
||||
|
||||
foreach ( $wc_admin_group_settings as $setting ) {
|
||||
if ( ! empty( $setting['id'] ) ) {
|
||||
$settings['wcAdminSettings'][ $setting['id'] ] = $setting['value'];
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object defining the currecy options for the site's current currency
|
||||
*
|
||||
* @return array Settings for the current currency {
|
||||
* Array of settings.
|
||||
*
|
||||
* @type string $code Currency code.
|
||||
* @type string $precision Number of decimals.
|
||||
* @type string $symbol Symbol for currency.
|
||||
* }
|
||||
*/
|
||||
public static function get_currency_settings() {
|
||||
$code = get_woocommerce_currency();
|
||||
|
||||
return apply_filters(
|
||||
'wc_currency_settings',
|
||||
array(
|
||||
'code' => $code,
|
||||
'precision' => wc_get_price_decimals(),
|
||||
'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ),
|
||||
'symbolPosition' => get_option( 'woocommerce_currency_pos' ),
|
||||
'decimalSeparator' => wc_get_price_decimal_separator(),
|
||||
'thousandSeparator' => wc_get_price_thousand_separator(),
|
||||
'priceFormat' => html_entity_decode( get_woocommerce_price_format() ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete woocommerce_onboarding_homepage_post_id field when the homepage is deleted
|
||||
*
|
||||
* @param int $post_id The deleted post id.
|
||||
*/
|
||||
public static function delete_homepage( $post_id ) {
|
||||
if ( 'page' !== get_post_type( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
$homepage_id = intval( get_option( 'woocommerce_onboarding_homepage_post_id', false ) );
|
||||
if ( $homepage_id === $post_id ) {
|
||||
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() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use WP_Filesystem_Direct;
|
||||
|
||||
/**
|
||||
* File class.
|
||||
*
|
||||
* An object representation of a single log file.
|
||||
*/
|
||||
class File {
|
||||
/**
|
||||
* The absolute path of the file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The source property of the file, derived from the filename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $source = '';
|
||||
|
||||
/**
|
||||
* The 0-based increment of the file, if it has been rotated. Derived from the filename. Can only be 0-9.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $rotation;
|
||||
|
||||
/**
|
||||
* The date the file was created, as a Unix timestamp, derived from the filename.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $created = 0;
|
||||
|
||||
/**
|
||||
* The hash property of the file, derived from the filename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $hash = '';
|
||||
|
||||
/**
|
||||
* The file's resource handle when it is open.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $stream;
|
||||
|
||||
/**
|
||||
* Class File
|
||||
*
|
||||
* @param string $path The absolute path of the file.
|
||||
*/
|
||||
public function __construct( $path ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->ingest_path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure open streams are closed.
|
||||
*/
|
||||
public function __destruct() {
|
||||
if ( is_resource( $this->stream ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
fclose( $this->stream );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a path to a log file to determine if it uses the standard filename structure and various properties.
|
||||
*
|
||||
* This makes assumptions about the structure of the log file's name. Using `-` to separate the name into segments,
|
||||
* if there are at least 5 segments, it assumes that the last segment is the hash, and the three segments before
|
||||
* that make up the date when the file was created in YYYY-MM-DD format. Any segments left after that are the
|
||||
* "source" that generated the log entries. If the filename doesn't have enough segments, it falls back to the
|
||||
* source and the hash both being the entire filename, and using the inode change time as the creation date.
|
||||
*
|
||||
* Example:
|
||||
* my-custom-plugin.2-2025-01-01-a1b2c3d4e5f.log
|
||||
* | | | |
|
||||
* 'my-custom-plugin' | '2025-01-01' |
|
||||
* (source) | (created) |
|
||||
* '2' 'a1b2c3d4e5f'
|
||||
* (rotation) (hash)
|
||||
*
|
||||
* @param string $path The full path of the log file.
|
||||
*
|
||||
* @return array {
|
||||
* @type string $dirname The directory structure containing the file. See pathinfo().
|
||||
* @type string $basename The filename with extension. See pathinfo().
|
||||
* @type string $extension The file extension. See pathinfo().
|
||||
* @type string $filename The filename without extension. See pathinfo().
|
||||
* @type string $source The source of the log entries contained in the file.
|
||||
* @type int|null $rotation The 0-based incremental rotation marker, if the file has been rotated.
|
||||
* Should only be a single digit.
|
||||
* @type int $created The date the file was created, as a Unix timestamp.
|
||||
* @type string $hash The hash suffix of the filename that protects from direct access.
|
||||
* @type string $file_id The public ID of the log file (filename without the hash).
|
||||
* }
|
||||
*/
|
||||
public static function parse_path( string $path ): array {
|
||||
$defaults = array(
|
||||
'dirname' => '',
|
||||
'basename' => '',
|
||||
'extension' => '',
|
||||
'filename' => '',
|
||||
'source' => '',
|
||||
'rotation' => null,
|
||||
'created' => 0,
|
||||
'hash' => '',
|
||||
'file_id' => '',
|
||||
);
|
||||
|
||||
$parsed = array_merge( $defaults, pathinfo( $path ) );
|
||||
|
||||
$segments = explode( '-', $parsed['filename'] );
|
||||
$timestamp = strtotime( implode( '-', array_slice( $segments, -4, 3 ) ) );
|
||||
|
||||
if ( count( $segments ) >= 5 && false !== $timestamp ) {
|
||||
$parsed['source'] = implode( '-', array_slice( $segments, 0, -4 ) );
|
||||
$parsed['created'] = $timestamp;
|
||||
$parsed['hash'] = array_slice( $segments, -1 )[0];
|
||||
} else {
|
||||
$parsed['source'] = implode( '-', $segments );
|
||||
}
|
||||
|
||||
$rotation_marker = strrpos( $parsed['source'], '.', -1 );
|
||||
if ( false !== $rotation_marker ) {
|
||||
$rotation = substr( $parsed['source'], -1 );
|
||||
if ( is_numeric( $rotation ) ) {
|
||||
$parsed['rotation'] = intval( $rotation );
|
||||
}
|
||||
|
||||
$parsed['source'] = substr( $parsed['source'], 0, $rotation_marker );
|
||||
}
|
||||
|
||||
$parsed['file_id'] = static::generate_file_id(
|
||||
$parsed['source'],
|
||||
$parsed['rotation'],
|
||||
$parsed['created']
|
||||
);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a public ID for a log file based on its properties.
|
||||
*
|
||||
* The file ID is the basename of the file without the hash part. It allows us to identify a file without revealing
|
||||
* its full name in the filesystem, so that it's difficult to access the file directly with an HTTP request.
|
||||
*
|
||||
* @param string $source The source of the log entries contained in the file.
|
||||
* @param int|null $rotation Optional. The 0-based incremental rotation marker, if the file has been rotated.
|
||||
* Should only be a single digit.
|
||||
* @param int $created Optional. The date the file was created, as a Unix timestamp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_file_id( string $source, ?int $rotation = null, int $created = 0 ): string {
|
||||
$file_id = static::sanitize_source( $source );
|
||||
|
||||
if ( ! is_null( $rotation ) ) {
|
||||
$file_id .= '.' . $rotation;
|
||||
}
|
||||
|
||||
if ( $created > 0 ) {
|
||||
$file_id .= '-' . gmdate( 'Y-m-d', $created );
|
||||
}
|
||||
|
||||
return $file_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash to use as the suffix on a log filename.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_hash( string $file_id ): string {
|
||||
$key = Constants::get_constant( 'AUTH_SALT' ) ?? 'wc-logs';
|
||||
|
||||
return hash_hmac( 'md5', $file_id, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the source property of a log file.
|
||||
*
|
||||
* @param string $source The source of the log entries contained in the file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_source( string $source ): string {
|
||||
return sanitize_file_name( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the log file path and assign various properties to this class instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ingest_path(): void {
|
||||
$parsed_path = static::parse_path( $this->path );
|
||||
$this->source = $parsed_path['source'];
|
||||
$this->rotation = $parsed_path['rotation'];
|
||||
$this->created = $parsed_path['created'];
|
||||
$this->hash = $parsed_path['hash'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the filename structure is in the expected format.
|
||||
*
|
||||
* @see parse_path().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_standard_filename(): bool {
|
||||
return ! ! $this->get_hash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file represented by the class instance is a file and is readable.
|
||||
*
|
||||
* @global WP_Filesystem_Direct $wp_filesystem
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_readable(): bool {
|
||||
global $wp_filesystem;
|
||||
|
||||
return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_readable( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file represented by the class instance is a file and is writable.
|
||||
*
|
||||
* @global WP_Filesystem_Direct $wp_filesystem
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_writable(): bool {
|
||||
global $wp_filesystem;
|
||||
|
||||
return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_writable( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a read-only stream for this file.
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function get_stream() {
|
||||
if ( ! $this->is_readable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_resource( $this->stream ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$this->stream = fopen( $this->path, 'rb' );
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream for this file.
|
||||
*
|
||||
* The stream will also close automatically when the class instance destructs, but this can be useful for
|
||||
* avoiding having a large number of streams open simultaneously.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function close_stream(): bool {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
return fclose( $this->stream );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full absolute path of the file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the file, with extension, but without full path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_basename(): string {
|
||||
return basename( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's source property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_source(): string {
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's rotation property.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function get_rotation(): ?int {
|
||||
return $this->rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's hash property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_hash(): string {
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's public ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_id(): string {
|
||||
$created = 0;
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$created = $this->get_created_timestamp();
|
||||
}
|
||||
|
||||
$file_id = static::generate_file_id(
|
||||
$this->get_source(),
|
||||
$this->get_rotation(),
|
||||
$created
|
||||
);
|
||||
|
||||
return $file_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's created property.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_created_timestamp(): int {
|
||||
if ( ! $this->created && $this->is_readable() ) {
|
||||
$this->created = filectime( $this->path );
|
||||
}
|
||||
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the last modification of the file, as a Unix timestamp. Or false if the file isn't readable.
|
||||
*
|
||||
* @global WP_Filesystem_Direct $wp_filesystem
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function get_modified_timestamp() {
|
||||
global $wp_filesystem;
|
||||
|
||||
return $wp_filesystem->mtime( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the file in bytes. Or false if the file isn't readable.
|
||||
*
|
||||
* @global WP_Filesystem_Direct $wp_filesystem
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function get_file_size() {
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( ! $wp_filesystem->is_readable( $this->path ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $wp_filesystem->size( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and set permissions on the file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function create(): bool {
|
||||
global $wp_filesystem;
|
||||
|
||||
$created = $wp_filesystem->touch( $this->path );
|
||||
$modded = $wp_filesystem->chmod( $this->path );
|
||||
|
||||
return $created && $modded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to the file, appending it to the end.
|
||||
*
|
||||
* @param string $text The content to add to the file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write( string $text ): bool {
|
||||
if ( ! $this->is_writable() ) {
|
||||
$created = $this->create();
|
||||
|
||||
if ( ! $created || ! $this->is_writable() ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure content ends with a line ending.
|
||||
$eol_pos = strrpos( $text, PHP_EOL );
|
||||
if ( false === $eol_pos || strlen( $text ) !== $eol_pos + 1 ) {
|
||||
$text .= PHP_EOL;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$resource = fopen( $this->path, 'ab' );
|
||||
|
||||
mbstring_binary_safe_encoding();
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- No suitable alternative.
|
||||
$bytes_written = fwrite( $resource, $text );
|
||||
reset_mbstring_encoding();
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
fclose( $resource );
|
||||
|
||||
if ( strlen( $text ) !== $bytes_written ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename this file with an incremented rotation number.
|
||||
*
|
||||
* @return bool True if the file was successfully rotated.
|
||||
*/
|
||||
public function rotate(): bool {
|
||||
if ( ! $this->is_writable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
|
||||
$created = 0;
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$created = $this->get_created_timestamp();
|
||||
}
|
||||
|
||||
if ( is_null( $this->get_rotation() ) ) {
|
||||
$new_rotation = 0;
|
||||
} else {
|
||||
$new_rotation = $this->get_rotation() + 1;
|
||||
}
|
||||
|
||||
$new_file_id = static::generate_file_id( $this->get_source(), $new_rotation, $created );
|
||||
|
||||
$search = array( $this->get_file_id() );
|
||||
$replace = array( $new_file_id );
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$search[] = $this->get_hash();
|
||||
$replace[] = static::generate_hash( $new_file_id );
|
||||
}
|
||||
|
||||
$old_filename = $this->get_basename();
|
||||
$new_filename = str_replace( $search, $replace, $old_filename );
|
||||
$new_path = str_replace( $old_filename, $new_filename, $this->path );
|
||||
|
||||
$moved = $wp_filesystem->move( $this->path, $new_path, true );
|
||||
if ( ! $moved ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->path = $new_path;
|
||||
$this->ingest_path();
|
||||
|
||||
return $this->is_readable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the file from the filesystem.
|
||||
*
|
||||
* @global WP_Filesystem_Direct $wp_filesystem
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function delete(): bool {
|
||||
global $wp_filesystem;
|
||||
|
||||
return $wp_filesystem->delete( $this->path, false, 'f' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\Settings;
|
||||
use PclZip;
|
||||
use WC_Cache_Helper;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* FileController class.
|
||||
*/
|
||||
class FileController {
|
||||
/**
|
||||
* The maximum number of rotations for a file before they start getting overwritten.
|
||||
*
|
||||
* This number should not go above 10, or it will cause issues with the glob patterns.
|
||||
*
|
||||
* const int
|
||||
*/
|
||||
private const MAX_FILE_ROTATIONS = 10;
|
||||
|
||||
/**
|
||||
* Default values for arguments for the get_files method.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
public const DEFAULTS_GET_FILES = array(
|
||||
'date_end' => 0,
|
||||
'date_filter' => '',
|
||||
'date_start' => 0,
|
||||
'offset' => 0,
|
||||
'order' => 'desc',
|
||||
'orderby' => 'modified',
|
||||
'per_page' => 20,
|
||||
'source' => '',
|
||||
);
|
||||
|
||||
/**
|
||||
* Default values for arguments for the search_within_files method.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
public const DEFAULTS_SEARCH_WITHIN_FILES = array(
|
||||
'offset' => 0,
|
||||
'per_page' => 50,
|
||||
);
|
||||
|
||||
/**
|
||||
* The maximum number of files that can be searched at one time.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
public const SEARCH_MAX_FILES = 100;
|
||||
|
||||
/**
|
||||
* The maximum number of search results that can be returned at one time.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
public const SEARCH_MAX_RESULTS = 200;
|
||||
|
||||
/**
|
||||
* The cache group name to use for caching operations.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const CACHE_GROUP = 'log-files';
|
||||
|
||||
/**
|
||||
* A cache key for storing and retrieving the results of the last logs search.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const SEARCH_CACHE_KEY = 'logs_previous_search';
|
||||
|
||||
/**
|
||||
* Get the file size limit that determines when to rotate a file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_file_size_limit(): int {
|
||||
$default = 5 * MB_IN_BYTES;
|
||||
|
||||
/**
|
||||
* Filter the threshold size of a log file at which point it will get rotated.
|
||||
*
|
||||
* @since 3.4.0
|
||||
*
|
||||
* @param int $file_size_limit The file size limit in bytes.
|
||||
*/
|
||||
$file_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $default );
|
||||
|
||||
if ( ! is_int( $file_size_limit ) || $file_size_limit < 1 ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $file_size_limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry to the appropriate file, after rotating the file if necessary.
|
||||
*
|
||||
* @param string $source The source property of the log entry, which determines which file to write to.
|
||||
* @param string $text The contents of the log entry to add to a file.
|
||||
* @param int|null $time Optional. The time of the log entry as a Unix timestamp. Defaults to the current time.
|
||||
*
|
||||
* @return bool True if the contents were successfully written to the file.
|
||||
*/
|
||||
public function write_to_file( string $source, string $text, ?int $time = null ): bool {
|
||||
if ( is_null( $time ) ) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
$file_id = File::generate_file_id( $source, null, $time );
|
||||
$file = $this->get_file_by_id( $file_id );
|
||||
|
||||
if ( $file instanceof File && $file->get_file_size() >= $this->get_file_size_limit() ) {
|
||||
$rotated = $this->rotate_file( $file->get_file_id() );
|
||||
|
||||
if ( $rotated ) {
|
||||
$file = null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $file instanceof File ) {
|
||||
$new_path = Settings::get_log_directory() . $this->generate_filename( $source, $time );
|
||||
$file = new File( $new_path );
|
||||
}
|
||||
|
||||
return $file->write( $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the full name of a file based on source and date values.
|
||||
*
|
||||
* @param string $source The source property of a log entry, which determines the filename.
|
||||
* @param int $time The time of the log entry as a Unix timestamp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_filename( string $source, int $time ): string {
|
||||
$file_id = File::generate_file_id( $source, null, $time );
|
||||
$hash = File::generate_hash( $file_id );
|
||||
|
||||
return "$file_id-$hash.log";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the rotations of a file and increment them, so that they overwrite the previous file with that rotation.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return bool True if the file and all its rotations were successfully rotated.
|
||||
*/
|
||||
private function rotate_file( $file_id ): bool {
|
||||
$rotations = $this->get_file_rotations( $file_id );
|
||||
|
||||
if ( is_wp_error( $rotations ) || ! isset( $rotations['current'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$max_rotation_marker = self::MAX_FILE_ROTATIONS - 1;
|
||||
|
||||
// Don't rotate a file with the maximum rotation.
|
||||
unset( $rotations[ $max_rotation_marker ] );
|
||||
|
||||
$results = array();
|
||||
// Rotate starting with oldest first and working backwards.
|
||||
for ( $i = $max_rotation_marker; $i >= 0; $i -- ) {
|
||||
if ( isset( $rotations[ $i ] ) ) {
|
||||
$results[] = $rotations[ $i ]->rotate();
|
||||
}
|
||||
}
|
||||
$results[] = $rotations['current']->rotate();
|
||||
|
||||
return ! in_array( false, $results, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of log files.
|
||||
*
|
||||
* @param array $args {
|
||||
* Optional. Arguments to filter and sort the files that are returned.
|
||||
*
|
||||
* @type int $date_end The end of the date range to filter by, as a Unix timestamp.
|
||||
* @type string $date_filter Filter files by one of the date props. 'created' or 'modified'.
|
||||
* @type int $date_start The beginning of the date range to filter by, as a Unix timestamp.
|
||||
* @type int $offset Omit this number of files from the beginning of the list. Works with $per_page to do pagination.
|
||||
* @type string $order The sort direction. 'asc' or 'desc'. Defaults to 'desc'.
|
||||
* @type string $orderby The property to sort the list by. 'created', 'modified', 'source', 'size'. Defaults to 'modified'.
|
||||
* @type int $per_page The number of files to include in the list. Works with $offset to do pagination.
|
||||
* @type string $source Only include files from this source.
|
||||
* }
|
||||
* @param bool $count_only Optional. True to return a total count of the files.
|
||||
*
|
||||
* @return File[]|int|WP_Error
|
||||
*/
|
||||
public function get_files( array $args = array(), bool $count_only = false ) {
|
||||
$args = wp_parse_args( $args, self::DEFAULTS_GET_FILES );
|
||||
|
||||
$pattern = $args['source'] . '*.log';
|
||||
$paths = glob( Settings::get_log_directory() . $pattern );
|
||||
|
||||
if ( false === $paths ) {
|
||||
return new WP_Error(
|
||||
'wc_log_directory_error',
|
||||
__( 'Could not access the log file directory.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$files = $this->convert_paths_to_objects( $paths );
|
||||
|
||||
if ( $args['date_filter'] && $args['date_start'] && $args['date_end'] ) {
|
||||
switch ( $args['date_filter'] ) {
|
||||
case 'created':
|
||||
$files = array_filter(
|
||||
$files,
|
||||
fn( $file ) => $file->get_created_timestamp() >= $args['date_start']
|
||||
&& $file->get_created_timestamp() <= $args['date_end']
|
||||
);
|
||||
break;
|
||||
case 'modified':
|
||||
$files = array_filter(
|
||||
$files,
|
||||
fn( $file ) => $file->get_modified_timestamp() >= $args['date_start']
|
||||
&& $file->get_modified_timestamp() <= $args['date_end']
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( true === $count_only ) {
|
||||
return count( $files );
|
||||
}
|
||||
|
||||
$multi_sorter = function( $sort_sets, $order_sets ) {
|
||||
$comparison = 0;
|
||||
|
||||
while ( ! empty( $sort_sets ) ) {
|
||||
$set = array_shift( $sort_sets );
|
||||
$order = array_shift( $order_sets );
|
||||
|
||||
if ( 'desc' === $order ) {
|
||||
$comparison = $set[1] <=> $set[0];
|
||||
} else {
|
||||
$comparison = $set[0] <=> $set[1];
|
||||
}
|
||||
|
||||
if ( 0 !== $comparison ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $comparison;
|
||||
};
|
||||
|
||||
switch ( $args['orderby'] ) {
|
||||
case 'created':
|
||||
$sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) {
|
||||
$sort_sets = array(
|
||||
array( $a->get_created_timestamp(), $b->get_created_timestamp() ),
|
||||
array( $a->get_source(), $b->get_source() ),
|
||||
array( $a->get_rotation() || -1, $b->get_rotation() || -1 ),
|
||||
);
|
||||
$order_sets = array( $args['order'], 'asc', 'asc' );
|
||||
return $multi_sorter( $sort_sets, $order_sets );
|
||||
};
|
||||
break;
|
||||
case 'modified':
|
||||
$sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) {
|
||||
$sort_sets = array(
|
||||
array( $a->get_modified_timestamp(), $b->get_modified_timestamp() ),
|
||||
array( $a->get_source(), $b->get_source() ),
|
||||
array( $a->get_rotation() || -1, $b->get_rotation() || -1 ),
|
||||
);
|
||||
$order_sets = array( $args['order'], 'asc', 'asc' );
|
||||
return $multi_sorter( $sort_sets, $order_sets );
|
||||
};
|
||||
break;
|
||||
case 'source':
|
||||
$sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) {
|
||||
$sort_sets = array(
|
||||
array( $a->get_source(), $b->get_source() ),
|
||||
array( $a->get_created_timestamp(), $b->get_created_timestamp() ),
|
||||
array( $a->get_rotation() || -1, $b->get_rotation() || -1 ),
|
||||
);
|
||||
$order_sets = array( $args['order'], 'desc', 'asc' );
|
||||
return $multi_sorter( $sort_sets, $order_sets );
|
||||
};
|
||||
break;
|
||||
case 'size':
|
||||
$sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) {
|
||||
$sort_sets = array(
|
||||
array( $a->get_file_size(), $b->get_file_size() ),
|
||||
array( $a->get_source(), $b->get_source() ),
|
||||
array( $a->get_rotation() || -1, $b->get_rotation() || -1 ),
|
||||
);
|
||||
$order_sets = array( $args['order'], 'asc', 'asc' );
|
||||
return $multi_sorter( $sort_sets, $order_sets );
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
usort( $files, $sort_callback );
|
||||
|
||||
return array_slice( $files, $args['offset'], $args['per_page'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one or more File instances from an array of file IDs.
|
||||
*
|
||||
* @param array $file_ids An array of file IDs (file basename without the hash).
|
||||
*
|
||||
* @return File[]
|
||||
*/
|
||||
public function get_files_by_id( array $file_ids ): array {
|
||||
$log_directory = Settings::get_log_directory();
|
||||
$paths = array();
|
||||
|
||||
foreach ( $file_ids as $file_id ) {
|
||||
// Look for the standard filename format first, which includes a hash.
|
||||
$glob = glob( $log_directory . $file_id . '-*.log' );
|
||||
|
||||
if ( ! $glob ) {
|
||||
$glob = glob( $log_directory . $file_id . '.log' );
|
||||
}
|
||||
|
||||
if ( is_array( $glob ) ) {
|
||||
$paths = array_merge( $paths, $glob );
|
||||
}
|
||||
}
|
||||
|
||||
$files = $this->convert_paths_to_objects( array_unique( $paths ) );
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a File instance from a file ID.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return File|WP_Error
|
||||
*/
|
||||
public function get_file_by_id( string $file_id ) {
|
||||
$result = $this->get_files_by_id( array( $file_id ) );
|
||||
|
||||
if ( count( $result ) < 1 ) {
|
||||
return new WP_Error(
|
||||
'wc_log_file_error',
|
||||
esc_html__( 'This file does not exist.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $result ) > 1 ) {
|
||||
return new WP_Error(
|
||||
'wc_log_file_error',
|
||||
esc_html__( 'Multiple files match this ID.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
return reset( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get File instances for a given file ID and all of its related rotations.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return File[]|WP_Error An associative array where the rotation integer of the file is the key, and a "current"
|
||||
* key for the iteration of the file that hasn't been rotated (if it exists).
|
||||
*/
|
||||
public function get_file_rotations( string $file_id ) {
|
||||
$file = $this->get_file_by_id( $file_id );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$current = array();
|
||||
$rotations = array();
|
||||
|
||||
$source = $file->get_source();
|
||||
$created = 0;
|
||||
if ( $file->has_standard_filename() ) {
|
||||
$created = $file->get_created_timestamp();
|
||||
}
|
||||
|
||||
if ( is_null( $file->get_rotation() ) ) {
|
||||
$current['current'] = $file;
|
||||
} else {
|
||||
$current_file_id = File::generate_file_id( $source, null, $created );
|
||||
$result = $this->get_file_by_id( $current_file_id );
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$current['current'] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$rotations_pattern = sprintf(
|
||||
'.[%s]',
|
||||
implode(
|
||||
'',
|
||||
range( 0, self::MAX_FILE_ROTATIONS - 1 )
|
||||
)
|
||||
);
|
||||
|
||||
$created_pattern = $created ? '-' . gmdate( 'Y-m-d', $created ) . '-' : '';
|
||||
|
||||
$rotation_pattern = Settings::get_log_directory() . $source . $rotations_pattern . $created_pattern . '*.log';
|
||||
$rotation_paths = glob( $rotation_pattern );
|
||||
$rotation_files = $this->convert_paths_to_objects( $rotation_paths );
|
||||
foreach ( $rotation_files as $rotation_file ) {
|
||||
if ( $rotation_file->is_readable() ) {
|
||||
$rotations[ $rotation_file->get_rotation() ] = $rotation_file;
|
||||
}
|
||||
}
|
||||
|
||||
ksort( $rotations );
|
||||
|
||||
return array_merge( $current, $rotations );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get an array of File instances.
|
||||
*
|
||||
* @param array $paths An array of absolute file paths.
|
||||
*
|
||||
* @return File[]
|
||||
*/
|
||||
private function convert_paths_to_objects( array $paths ): array {
|
||||
$files = array_map(
|
||||
function( $path ) {
|
||||
$file = new File( $path );
|
||||
return $file->is_readable() ? $file : null;
|
||||
},
|
||||
$paths
|
||||
);
|
||||
|
||||
return array_filter( $files );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of sources for existing log files.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_file_sources() {
|
||||
$paths = glob( Settings::get_log_directory() . '*.log' );
|
||||
if ( false === $paths ) {
|
||||
return new WP_Error(
|
||||
'wc_log_directory_error',
|
||||
__( 'Could not access the log file directory.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$all_sources = array_map(
|
||||
function( $path ) {
|
||||
$file = new File( $path );
|
||||
return $file->is_readable() ? $file->get_source() : null;
|
||||
},
|
||||
$paths
|
||||
);
|
||||
|
||||
return array_unique( array_filter( $all_sources ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one or more files from the filesystem.
|
||||
*
|
||||
* @param array $file_ids An array of file IDs (file basename without the hash).
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function delete_files( array $file_ids ): int {
|
||||
$deleted = 0;
|
||||
|
||||
$files = $this->get_files_by_id( $file_ids );
|
||||
foreach ( $files as $file ) {
|
||||
$result = false;
|
||||
if ( $file->is_writable() ) {
|
||||
$result = $file->delete();
|
||||
}
|
||||
|
||||
if ( true === $result ) {
|
||||
$deleted ++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->invalidate_cache();
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a single file to the browser without zipping it first.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function export_single_file( $file_id ) {
|
||||
$file = $this->get_file_by_id( $file_id );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$file_name = $file->get_file_id() . '.log';
|
||||
$exporter = new FileExporter( $file->get_path(), $file_name );
|
||||
|
||||
return $exporter->emit_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a zip archive of log files and stream it to the browser.
|
||||
*
|
||||
* @param array $file_ids An array of file IDs (file basename without the hash).
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function export_multiple_files( array $file_ids ) {
|
||||
$files = $this->get_files_by_id( $file_ids );
|
||||
|
||||
if ( count( $files ) < 1 ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_file',
|
||||
__( 'Could not access the specified files.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$temp_dir = get_temp_dir();
|
||||
|
||||
if ( ! is_dir( $temp_dir ) || ! wp_is_writable( $temp_dir ) ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_directory',
|
||||
__( 'Could not write to the temp directory. Try downloading files one at a time instead.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
|
||||
|
||||
$path = trailingslashit( $temp_dir ) . 'woocommerce_logs_' . gmdate( 'Y-m-d_H-i-s' ) . '.zip';
|
||||
$file_paths = array_map(
|
||||
fn( $file ) => $file->get_path(),
|
||||
$files
|
||||
);
|
||||
$archive = new PclZip( $path );
|
||||
|
||||
$archive->create( $file_paths, PCLZIP_OPT_REMOVE_ALL_PATH );
|
||||
|
||||
$exporter = new FileExporter( $path );
|
||||
|
||||
return $exporter->emit_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search within a set of log files for a particular string.
|
||||
*
|
||||
* @param string $search The string to search for.
|
||||
* @param array $args Optional. Arguments for pagination of search results.
|
||||
* @param array $file_args Optional. Arguments to filter and sort the files that are returned. See get_files().
|
||||
* @param bool $count_only Optional. True to return a total count of the matches.
|
||||
*
|
||||
* @return array|int|WP_Error When matches are found, each array item is an associative array that includes the
|
||||
* file ID, line number, and the matched string with HTML markup around the matched parts.
|
||||
*/
|
||||
public function search_within_files( string $search, array $args = array(), array $file_args = array(), bool $count_only = false ) {
|
||||
if ( '' === $search ) {
|
||||
return $count_only ? 0 : array();
|
||||
}
|
||||
|
||||
$search = esc_html( $search );
|
||||
|
||||
$args = wp_parse_args( $args, self::DEFAULTS_SEARCH_WITHIN_FILES );
|
||||
|
||||
$file_args = array_merge(
|
||||
$file_args,
|
||||
array(
|
||||
'offset' => 0,
|
||||
'per_page' => self::SEARCH_MAX_FILES,
|
||||
)
|
||||
);
|
||||
|
||||
$cache_key = WC_Cache_Helper::get_prefixed_key( self::SEARCH_CACHE_KEY, self::CACHE_GROUP );
|
||||
$query = wp_json_encode( array( $search, $args, $file_args ) );
|
||||
$cache = wp_cache_get( $cache_key );
|
||||
$is_cached = isset( $cache['query'], $cache['results'] ) && $query === $cache['query'];
|
||||
|
||||
if ( true === $is_cached ) {
|
||||
$matched_lines = $cache['results'];
|
||||
} else {
|
||||
$files = $this->get_files( $file_args );
|
||||
if ( is_wp_error( $files ) ) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Max string size * SEARCH_MAX_RESULTS = ~1MB largest possible cache entry.
|
||||
$max_string_size = 5 * KB_IN_BYTES;
|
||||
|
||||
$matched_lines = array();
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$stream = $file->get_stream();
|
||||
$line_number = 1;
|
||||
|
||||
while ( ! feof( $stream ) ) {
|
||||
$line = fgets( $stream, $max_string_size );
|
||||
if ( ! is_string( $line ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized_line = esc_html( trim( $line ) );
|
||||
if ( false !== stripos( $sanitized_line, $search ) ) {
|
||||
$matched_lines[] = array(
|
||||
'file_id' => $file->get_file_id(),
|
||||
'line_number' => $line_number,
|
||||
'line' => $sanitized_line,
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $matched_lines ) >= self::SEARCH_MAX_RESULTS ) {
|
||||
$file->close_stream();
|
||||
break 2;
|
||||
}
|
||||
|
||||
if ( false !== strstr( $line, PHP_EOL ) ) {
|
||||
$line_number ++;
|
||||
}
|
||||
}
|
||||
|
||||
$file->close_stream();
|
||||
}
|
||||
|
||||
$to_cache = array(
|
||||
'query' => $query,
|
||||
'results' => $matched_lines,
|
||||
);
|
||||
wp_cache_set( $cache_key, $to_cache, self::CACHE_GROUP, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
if ( true === $count_only ) {
|
||||
return count( $matched_lines );
|
||||
}
|
||||
|
||||
return array_slice( $matched_lines, $args['offset'], $args['per_page'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size, in bytes, of the log directory.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_log_directory_size(): int {
|
||||
$bytes = 0;
|
||||
$path = realpath( Settings::get_log_directory() );
|
||||
|
||||
if ( wp_is_writable( $path ) ) {
|
||||
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ) );
|
||||
|
||||
foreach ( $iterator as $file ) {
|
||||
$bytes += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cache group related to log file data.
|
||||
*
|
||||
* @return bool True on successfully invalidating the cache.
|
||||
*/
|
||||
public function invalidate_cache(): bool {
|
||||
return WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Filesystem_Direct;
|
||||
|
||||
/**
|
||||
* FileExport class.
|
||||
*/
|
||||
class FileExporter {
|
||||
/**
|
||||
* The number of bytes per read while streaming the file.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
private const CHUNK_SIZE = 4 * KB_IN_BYTES;
|
||||
|
||||
/**
|
||||
* The absolute path of the file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* A name of the file to send to the browser rather than the filename part of the path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $alternate_filename;
|
||||
|
||||
/**
|
||||
* Class FileExporter.
|
||||
*
|
||||
* @param string $path The absolute path of the file.
|
||||
* @param string $alternate_filename Optional. The name of the file to send to the browser rather than the filename
|
||||
* part of the path.
|
||||
*/
|
||||
public function __construct( string $path, string $alternate_filename = '' ) {
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->alternate_filename = $alternate_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure PHP and stream the file to the browser.
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function emit_file() {
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem->is_file( $this->path ) || ! $wp_filesystem->is_readable( $this->path ) ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_file',
|
||||
__( 'Could not access file.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
// These configuration tweaks are copied from WC_CSV_Exporter::send_headers().
|
||||
// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
if ( function_exists( 'gc_enable' ) ) {
|
||||
gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound
|
||||
}
|
||||
if ( function_exists( 'apache_setenv' ) ) {
|
||||
@apache_setenv( 'no-gzip', '1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv
|
||||
}
|
||||
@ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
@ini_set( 'output_buffering', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
@ini_set( 'output_handler', '' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
ignore_user_abort( true );
|
||||
wc_set_time_limit();
|
||||
wc_nocache_headers();
|
||||
// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
$this->send_headers();
|
||||
$this->send_contents();
|
||||
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HTTP headers at the beginning of a file.
|
||||
*
|
||||
* Modeled on WC_CSV_Exporter::send_headers().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function send_headers(): void {
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename=' . $this->get_filename() );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the contents of the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function send_contents(): void {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$stream = fopen( $this->path, 'rb' );
|
||||
|
||||
while ( is_resource( $stream ) && ! feof( $stream ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- No suitable alternative.
|
||||
$chunk = fread( $stream, self::CHUNK_SIZE );
|
||||
|
||||
if ( is_string( $chunk ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputting to file.
|
||||
echo $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
fclose( $stream );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the file that will be sent to the browser.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_filename(): string {
|
||||
if ( $this->alternate_filename ) {
|
||||
return $this->alternate_filename;
|
||||
}
|
||||
|
||||
return basename( $this->path );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\PageController;
|
||||
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* FileListTable class.
|
||||
*/
|
||||
class FileListTable extends WP_List_Table {
|
||||
/**
|
||||
* The user option key for saving the preferred number of files displayed per page.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_file_list_per_page';
|
||||
|
||||
/**
|
||||
* Instance of FileController.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of PageController.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $page_controller;
|
||||
|
||||
/**
|
||||
* FileListTable class.
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param PageController $page_controller Instance of PageController.
|
||||
*/
|
||||
public function __construct( FileController $file_controller, PageController $page_controller ) {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->page_controller = $page_controller;
|
||||
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'log-file',
|
||||
'plural' => 'log-files',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render message when there are no items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function no_items(): void {
|
||||
esc_html_e( 'No log files found.', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of bulk actions available for this table.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_bulk_actions(): array {
|
||||
return array(
|
||||
'export' => esc_html__( 'Download', 'woocommerce' ),
|
||||
'delete' => esc_html__( 'Delete permanently', 'woocommerce' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing log sources for the filter dropdown.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_sources_list(): array {
|
||||
$sources = $this->file_controller->get_file_sources();
|
||||
if ( is_wp_error( $sources ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
sort( $sources );
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays extra controls between bulk actions and pagination.
|
||||
*
|
||||
* @param string $which The location of the tablenav being rendered. 'top' or 'bottom'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extra_tablenav( $which ): void {
|
||||
$all_sources = $this->get_sources_list();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
|
||||
$current_source = File::sanitize_source( wp_unslash( $_GET['source'] ?? '' ) );
|
||||
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<?php if ( 'top' === $which ) : ?>
|
||||
<label for="filter-by-source" class="screen-reader-text"><?php esc_html_e( 'Filter by log source', 'woocommerce' ); ?></label>
|
||||
<select name="source" id="filter-by-source">
|
||||
<option<?php selected( $current_source, '' ); ?> value=""><?php esc_html_e( 'All sources', 'woocommerce' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option<?php selected( $current_source, $source ); ?> value="<?php echo esc_attr( $source ); ?>">
|
||||
<?php echo esc_html( $source ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
submit_button(
|
||||
__( 'Filter', 'woocommerce' ),
|
||||
'',
|
||||
'filter_action',
|
||||
false,
|
||||
array(
|
||||
'id' => 'logs-filter-submit',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the column header info.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_column_headers(): void {
|
||||
$this->_column_headers = array(
|
||||
$this->get_columns(),
|
||||
get_hidden_columns( $this->screen ),
|
||||
$this->get_sortable_columns(),
|
||||
$this->get_primary_column(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_items(): void {
|
||||
$per_page = $this->get_items_per_page(
|
||||
self::PER_PAGE_USER_OPTION_KEY,
|
||||
$this->get_per_page_default()
|
||||
);
|
||||
|
||||
$defaults = array(
|
||||
'per_page' => $per_page,
|
||||
'offset' => ( $this->get_pagenum() - 1 ) * $per_page,
|
||||
);
|
||||
$file_args = wp_parse_args(
|
||||
$this->page_controller->get_query_params( array( 'order', 'orderby', 'source' ) ),
|
||||
$defaults
|
||||
);
|
||||
|
||||
$total_items = $this->file_controller->get_files( $file_args, true );
|
||||
if ( is_wp_error( $total_items ) ) {
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s</p></div>',
|
||||
esc_html( $total_items->get_error_message() )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total_items / $per_page );
|
||||
$items = $this->file_controller->get_files( $file_args );
|
||||
|
||||
$this->items = $items;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'per_page' => $per_page,
|
||||
'total_items' => $total_items,
|
||||
'total_pages' => $total_pages,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns(): array {
|
||||
$columns = array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'source' => esc_html__( 'Source', 'woocommerce' ),
|
||||
'created' => esc_html__( 'Date created', 'woocommerce' ),
|
||||
'modified' => esc_html__( 'Date modified', 'woocommerce' ),
|
||||
'size' => esc_html__( 'File size', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of sortable columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_sortable_columns(): array {
|
||||
$sortable = array(
|
||||
'source' => array( 'source' ),
|
||||
'created' => array( 'created' ),
|
||||
'modified' => array( 'modified', true ),
|
||||
'size' => array( 'size' ),
|
||||
);
|
||||
|
||||
return $sortable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the checkbox column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<input
|
||||
id="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>"
|
||||
type="checkbox"
|
||||
name="file_id[]"
|
||||
value="<?php echo esc_attr( $item->get_file_id() ); ?>"
|
||||
/>
|
||||
<label for="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>">
|
||||
<span class="screen-reader-text">
|
||||
<?php
|
||||
printf(
|
||||
// translators: 1. a date, 2. a slug-style name for a file.
|
||||
esc_html__( 'Select the %1$s log file for %2$s', 'woocommerce' ),
|
||||
esc_html( gmdate( get_option( 'date_format' ), $item->get_created_timestamp() ) ),
|
||||
esc_html( $item->get_source() )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</label>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the source column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_source( $item ): string {
|
||||
$log_file = $item->get_file_id();
|
||||
$single_file_url = add_query_arg(
|
||||
array(
|
||||
'view' => 'single_file',
|
||||
'file_id' => $log_file,
|
||||
),
|
||||
$this->page_controller->get_logs_tab_url()
|
||||
);
|
||||
$rotation = '';
|
||||
if ( ! is_null( $item->get_rotation() ) ) {
|
||||
$rotation = sprintf(
|
||||
' – <span class="post-state">%d</span>',
|
||||
$item->get_rotation()
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a class="row-title" href="%1$s">%2$s</a>%3$s',
|
||||
esc_url( $single_file_url ),
|
||||
esc_html( $item->get_source() ),
|
||||
$rotation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the created column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_created( $item ): string {
|
||||
$timestamp = $item->get_created_timestamp();
|
||||
|
||||
return gmdate( 'Y-m-d', $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modified column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_modified( $item ): string {
|
||||
$timestamp = $item->get_modified_timestamp();
|
||||
|
||||
return gmdate( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the size column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_size( $item ): string {
|
||||
$size = $item->get_file_size();
|
||||
|
||||
return size_format( $size );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the default value for the per_page arg.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_per_page_default(): int {
|
||||
return $this->file_controller::DEFAULTS_GET_FILES['per_page'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\PageController;
|
||||
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* SearchListTable class.
|
||||
*/
|
||||
class SearchListTable extends WP_List_Table {
|
||||
/**
|
||||
* The user option key for saving the preferred number of search results displayed per page.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_search_results_per_page';
|
||||
|
||||
/**
|
||||
* Instance of FileController.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of PageController.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $page_controller;
|
||||
|
||||
/**
|
||||
* SearchListTable class.
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param PageController $page_controller Instance of PageController.
|
||||
*/
|
||||
public function __construct( FileController $file_controller, PageController $page_controller ) {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->page_controller = $page_controller;
|
||||
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'wc-logs-search-result',
|
||||
'plural' => 'wc-logs-search-results',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render message when there are no items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function no_items(): void {
|
||||
esc_html_e( 'No search results.', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the column header info.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_column_headers(): void {
|
||||
$this->_column_headers = array(
|
||||
$this->get_columns(),
|
||||
array(),
|
||||
array(),
|
||||
$this->get_primary_column(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_items(): void {
|
||||
$per_page = $this->get_items_per_page(
|
||||
self::PER_PAGE_USER_OPTION_KEY,
|
||||
$this->get_per_page_default()
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'per_page' => $per_page,
|
||||
'offset' => ( $this->get_pagenum() - 1 ) * $per_page,
|
||||
);
|
||||
|
||||
$file_args = $this->page_controller->get_query_params(
|
||||
array( 'date_end', 'date_filter', 'date_start', 'order', 'orderby', 'search', 'source' )
|
||||
);
|
||||
$search = $file_args['search'];
|
||||
unset( $file_args['search'] );
|
||||
|
||||
$total_items = $this->file_controller->search_within_files( $search, $args, $file_args, true );
|
||||
if ( is_wp_error( $total_items ) ) {
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s</p></div>',
|
||||
esc_html( $total_items->get_error_message() )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $total_items >= $this->file_controller::SEARCH_MAX_RESULTS ) {
|
||||
printf(
|
||||
'<div class="notice notice-info"><p>%s</p></div>',
|
||||
sprintf(
|
||||
// translators: %s is a number.
|
||||
esc_html__( 'The number of search results has reached the limit of %s. Try refining your search.', 'woocommerce' ),
|
||||
esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_RESULTS ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total_items / $per_page );
|
||||
$results = $this->file_controller->search_within_files( $search, $args, $file_args );
|
||||
$this->items = $results;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'per_page' => $per_page,
|
||||
'total_items' => $total_items,
|
||||
'total_pages' => $total_pages,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns(): array {
|
||||
$columns = array(
|
||||
'file_id' => esc_html__( 'File', 'woocommerce' ),
|
||||
'line_number' => esc_html__( 'Line #', 'woocommerce' ),
|
||||
'line' => esc_html__( 'Matched Line', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the file_id column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_file_id( array $item ): string {
|
||||
// Add a word break after the rotation number, if it exists.
|
||||
$file_id = preg_replace( '/\.([0-9])+\-/', '.\1<wbr>-', $item['file_id'] );
|
||||
|
||||
return wp_kses( $file_id, array( 'wbr' => array() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the line_number column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_line_number( array $item ): string {
|
||||
$match_url = add_query_arg(
|
||||
array(
|
||||
'view' => 'single_file',
|
||||
'file_id' => $item['file_id'],
|
||||
),
|
||||
$this->page_controller->get_logs_tab_url() . '#L' . absint( $item['line_number'] )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<a href="%1$s">%2$s</a>',
|
||||
esc_url( $match_url ),
|
||||
sprintf(
|
||||
// translators: %s is a line number in a file.
|
||||
esc_html__( 'Line %s', 'woocommerce' ),
|
||||
number_format_i18n( absint( $item['line_number'] ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the line column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_line( array $item ): string {
|
||||
$params = $this->page_controller->get_query_params( array( 'search' ) );
|
||||
$line = $item['line'];
|
||||
|
||||
// Highlight matches within the line.
|
||||
$pattern = preg_quote( $params['search'], '/' );
|
||||
preg_match_all( "/$pattern/i", $line, $matches, PREG_OFFSET_CAPTURE );
|
||||
if ( is_array( $matches[0] ) && count( $matches[0] ) >= 1 ) {
|
||||
$length_change = 0;
|
||||
|
||||
foreach ( $matches[0] as $match ) {
|
||||
$replace = '<span class="search-match">' . $match[0] . '</span>';
|
||||
$offset = $match[1] + $length_change;
|
||||
$orig_length = strlen( $match[0] );
|
||||
$replace_length = strlen( $replace );
|
||||
|
||||
$line = substr_replace( $line, $replace, $offset, $orig_length );
|
||||
|
||||
$length_change += $replace_length - $orig_length;
|
||||
}
|
||||
}
|
||||
|
||||
return wp_kses_post( $line );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the default value for the per_page arg.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_per_page_default(): int {
|
||||
return $this->file_controller::DEFAULTS_SEARCH_WITHIN_FILES['per_page'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController };
|
||||
use WC_Log_Handler;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
/**
|
||||
* Instance of the FileController class.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of the Settings class.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->file_controller = wc_get_container()->get( FileController::class );
|
||||
$this->settings = wc_get_container()->get( Settings::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a log entry.
|
||||
*
|
||||
* @param int $timestamp Log timestamp.
|
||||
* @param string $level emergency|alert|critical|error|warning|notice|info|debug.
|
||||
* @param string $message Log message.
|
||||
* @param array $context {
|
||||
* Optional. Additional information for log handlers. Any data can be added here, but there are some array
|
||||
* keys that have special behavior.
|
||||
*
|
||||
* @type string $source Determines which log file to write to. Must be at least 3 characters in length.
|
||||
* @type bool $backtrace True to include a backtrace that shows where the logging function got called.
|
||||
* }
|
||||
*
|
||||
* @return bool False if value was not handled and true if value was handled.
|
||||
*/
|
||||
public function handle( $timestamp, $level, $message, $context ) {
|
||||
if ( isset( $context['source'] ) && is_string( $context['source'] ) && strlen( $context['source'] ) >= 3 ) {
|
||||
$source = sanitize_title( trim( $context['source'] ) );
|
||||
} else {
|
||||
$source = $this->determine_source();
|
||||
}
|
||||
|
||||
$entry = static::format_entry( $timestamp, $level, $message, $context );
|
||||
|
||||
$written = $this->file_controller->write_to_file( $source, $entry, $timestamp );
|
||||
|
||||
if ( $written ) {
|
||||
$this->file_controller->invalidate_cache();
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a log entry text from level, timestamp, and message.
|
||||
*
|
||||
* @param int $timestamp Log timestamp.
|
||||
* @param string $level emergency|alert|critical|error|warning|notice|info|debug.
|
||||
* @param string $message Log message.
|
||||
* @param array $context Additional information for log handlers.
|
||||
*
|
||||
* @return string Formatted log entry.
|
||||
*/
|
||||
protected static function format_entry( $timestamp, $level, $message, $context ) {
|
||||
$time_string = static::format_time( $timestamp );
|
||||
$level_string = strtoupper( $level );
|
||||
|
||||
unset( $context['source'] );
|
||||
if ( ! empty( $context ) ) {
|
||||
if ( isset( $context['backtrace'] ) && true === filter_var( $context['backtrace'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$context['backtrace'] = static::get_backtrace();
|
||||
}
|
||||
|
||||
$formatted_context = wp_json_encode( $context );
|
||||
$message .= " CONTEXT: $formatted_context";
|
||||
}
|
||||
|
||||
$entry = "$time_string $level_string $message";
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/abstracts/abstract-wc-log-handler.php */
|
||||
return apply_filters(
|
||||
'woocommerce_format_log_entry',
|
||||
$entry,
|
||||
array(
|
||||
'timestamp' => $timestamp,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
)
|
||||
);
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out a source string to use for a log entry based on where the log method was called from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function determine_source(): string {
|
||||
$source_roots = array(
|
||||
'mu-plugin' => trailingslashit( Constants::get_constant( 'WPMU_PLUGIN_DIR' ) ),
|
||||
'plugin' => trailingslashit( Constants::get_constant( 'WP_PLUGIN_DIR' ) ),
|
||||
'theme' => trailingslashit( get_theme_root() ),
|
||||
);
|
||||
|
||||
$source = '';
|
||||
$backtrace = static::get_backtrace();
|
||||
|
||||
foreach ( $backtrace as $frame ) {
|
||||
if ( ! isset( $frame['file'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $source_roots as $type => $path ) {
|
||||
if ( 0 === strpos( $frame['file'], $path ) ) {
|
||||
$relative_path = trim( substr( $frame['file'], strlen( $path ) ), DIRECTORY_SEPARATOR );
|
||||
|
||||
if ( 'mu-plugin' === $type ) {
|
||||
$info = pathinfo( $relative_path );
|
||||
|
||||
if ( '.' === $info['dirname'] ) {
|
||||
$source = "$type-" . $info['filename'];
|
||||
} else {
|
||||
$source = "$type-" . $info['dirname'];
|
||||
}
|
||||
|
||||
break 2;
|
||||
}
|
||||
|
||||
$segments = explode( DIRECTORY_SEPARATOR, $relative_path );
|
||||
if ( is_array( $segments ) ) {
|
||||
$source = "$type-" . reset( $segments );
|
||||
}
|
||||
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $source ) {
|
||||
$source = 'log';
|
||||
}
|
||||
|
||||
return sanitize_title( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs from a specific source.
|
||||
*
|
||||
* @param string $source The source of the log entries.
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function clear( string $source ): int {
|
||||
$source = File::sanitize_source( $source );
|
||||
|
||||
$files = $this->file_controller->get_files(
|
||||
array(
|
||||
'source' => $source,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file_ids = array_map(
|
||||
fn( $file ) => $file->get_file_id(),
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %1$s is a number of log files, %2$s is a slug-style name for a file.
|
||||
_n(
|
||||
'%1$s log file from source %2$s was deleted.',
|
||||
'%1$s log files from source %2$s were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $source )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
'backtrace' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs older than a specified timestamp.
|
||||
*
|
||||
* @param int $timestamp All files created before this timestamp will be deleted.
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function delete_logs_before_timestamp( int $timestamp = 0 ): int {
|
||||
if ( ! $timestamp ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$files = $this->file_controller->get_files(
|
||||
array(
|
||||
'date_filter' => 'created',
|
||||
'date_start' => 1,
|
||||
'date_end' => $timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$files = array_filter(
|
||||
$files,
|
||||
function( $file ) use ( $timestamp ) {
|
||||
/**
|
||||
* Allows preventing an expired log file from being deleted.
|
||||
*
|
||||
* @param bool $delete True to delete the file.
|
||||
* @param File $file The log file object.
|
||||
* @param int $timestamp The expiration threshold.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
$delete = apply_filters( 'woocommerce_logger_delete_expired_file', true, $file, $timestamp );
|
||||
|
||||
return boolval( $delete );
|
||||
}
|
||||
);
|
||||
|
||||
if ( count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file_ids = array_map(
|
||||
fn( $file ) => $file->get_file_id(),
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
$retention_days = $this->settings->get_retention_period();
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %s is a number of log files.
|
||||
_n(
|
||||
'%s expired log file was deleted.',
|
||||
'%s expired log files were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted )
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\{ LogHandlerFileV2, Settings };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable };
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Status;
|
||||
use WC_Log_Handler_File, WC_Log_Handler_DB;
|
||||
use WC_Log_Levels;
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* PageController class.
|
||||
*/
|
||||
class PageController {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Instance of FileController.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of Settings.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Instance of FileListTable or SearchListTable.
|
||||
*
|
||||
* @var FileListTable|SearchListTable
|
||||
*/
|
||||
private $list_table;
|
||||
|
||||
/**
|
||||
* Initialize dependencies.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param Settings $settings Instance of Settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function init(
|
||||
FileController $file_controller,
|
||||
Settings $settings
|
||||
): void {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->settings = $settings;
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add callbacks to hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'maybe_do_logs_tab_action' ), 2 );
|
||||
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'setup_screen_options' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'handle_list_table_bulk_actions' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'notices' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current tab on the Status page is Logs, and if so, fire an action.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_do_logs_tab_action(): void {
|
||||
$is_logs_tab = 'logs' === filter_input( INPUT_GET, 'tab' );
|
||||
|
||||
if ( $is_logs_tab ) {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
/**
|
||||
* Action fires when the Logs tab starts loading.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_load_tab', $params['view'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notices to display on Logs screens.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function notices() {
|
||||
if ( ! $this->settings->logging_is_enabled() ) {
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function() {
|
||||
?>
|
||||
<div class="notice notice-warning">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a URL to another admin screen.
|
||||
wp_kses_post( __( 'Logging is disabled. It can be enabled in <a href="%s">Logs Settings</a>.', 'woocommerce' ) ),
|
||||
esc_url( add_query_arg( 'view', 'settings', $this->get_logs_tab_url() ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonical URL for the Logs tab of the Status admin page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_logs_tab_url(): string {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-status',
|
||||
'tab' => 'logs',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Logs" tab, depending on the current default log handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render(): void {
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
$this->render_section_nav();
|
||||
|
||||
if ( 'settings' === $params['view'] ) {
|
||||
$this->settings->render_form();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
$this->render_filev2();
|
||||
return;
|
||||
case WC_Log_Handler_DB::class:
|
||||
WC_Admin_Status::status_logs_db();
|
||||
return;
|
||||
case WC_Log_Handler_File::class:
|
||||
WC_Admin_Status::status_logs_file();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action fires only if there is not a built-in rendering method for the current default log handler.
|
||||
*
|
||||
* This is intended as a way for extensions to render log views for custom handlers.
|
||||
*
|
||||
* @param string $handler
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_render_page', $handler );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render navigation to switch between logs browsing and settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_section_nav(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$browse_url = $this->get_logs_tab_url();
|
||||
$settings_url = add_query_arg( 'view', 'settings', $this->get_logs_tab_url() );
|
||||
|
||||
?>
|
||||
<ul class="subsubsub">
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $browse_url ),
|
||||
'settings' !== $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Browse', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
|
|
||||
</li>
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $settings_url ),
|
||||
'settings' === $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Settings', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
</li>
|
||||
</ul>
|
||||
<br class="clear">
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the views for the FileV2 log handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_filev2(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
switch ( $params['view'] ) {
|
||||
case 'list_files':
|
||||
default:
|
||||
$this->render_list_files_view();
|
||||
break;
|
||||
case 'search_results':
|
||||
$this->render_search_results_view();
|
||||
break;
|
||||
case 'single_file':
|
||||
$this->render_single_file_view();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the file list view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_list_files_view(): void {
|
||||
$params = $this->get_query_params( array( 'order', 'orderby', 'source', 'view' ) );
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2>
|
||||
<?php esc_html_e( 'Browse log files', 'woocommerce' ); ?>
|
||||
</h2>
|
||||
<?php $this->render_search_field(); ?>
|
||||
</header>
|
||||
<form id="logs-list-table-form" method="get">
|
||||
<input type="hidden" name="page" value="wc-status" />
|
||||
<input type="hidden" name="tab" value="logs" />
|
||||
<?php foreach ( $params as $key => $value ) : ?>
|
||||
<?php if ( $value !== $defaults[ $key ] ) : ?>
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $value ); ?>"
|
||||
/>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php $list_table->display(); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the single file view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_single_file_view(): void {
|
||||
$params = $this->get_query_params( array( 'file_id', 'view' ) );
|
||||
$file = $this->file_controller->get_file_by_id( $params['file_id'] );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
?>
|
||||
<div class="notice notice-error notice-inline">
|
||||
<?php echo wp_kses_post( wpautop( $file->get_error_message() ) ); ?>
|
||||
<?php
|
||||
printf(
|
||||
'<p><a href="%1$s">%2$s</a></p>',
|
||||
esc_url( $this->get_logs_tab_url() ),
|
||||
esc_html__( 'Return to the file list.', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$rotations = $this->file_controller->get_file_rotations( $file->get_file_id() );
|
||||
$rotation_url_base = add_query_arg( 'view', 'single_file', $this->get_logs_tab_url() );
|
||||
|
||||
$download_url = add_query_arg(
|
||||
array(
|
||||
'action' => 'export',
|
||||
'file_id' => array( $file->get_file_id() ),
|
||||
),
|
||||
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
|
||||
);
|
||||
$delete_url = add_query_arg(
|
||||
array(
|
||||
'action' => 'delete',
|
||||
'file_id' => array( $file->get_file_id() ),
|
||||
),
|
||||
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
|
||||
);
|
||||
$delete_confirmation_js = sprintf(
|
||||
"return window.confirm( '%s' )",
|
||||
esc_js( __( 'Delete this log file permanently?', 'woocommerce' ) )
|
||||
);
|
||||
|
||||
$stream = $file->get_stream();
|
||||
$line_number = 1;
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2>
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is the name of a log file.
|
||||
esc_html__( 'Viewing log file %s', 'woocommerce' ),
|
||||
sprintf(
|
||||
'<span class="file-id">%s</span>',
|
||||
esc_html( $file->get_file_id() )
|
||||
)
|
||||
);
|
||||
?>
|
||||
</h2>
|
||||
<?php if ( count( $rotations ) > 1 ) : ?>
|
||||
<nav class="wc-logs-single-file-rotations">
|
||||
<h3><?php esc_html_e( 'File rotations:', 'woocommerce' ); ?></h3>
|
||||
<ul class="wc-logs-rotation-links">
|
||||
<?php if ( isset( $rotations['current'] ) ) : ?>
|
||||
<?php
|
||||
printf(
|
||||
'<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>',
|
||||
esc_url( add_query_arg( 'file_id', $rotations['current']->get_file_id(), $rotation_url_base ) ),
|
||||
$file->get_file_id() === $rotations['current']->get_file_id() ? 'primary' : 'secondary',
|
||||
esc_html__( 'Current', 'woocommerce' )
|
||||
);
|
||||
unset( $rotations['current'] );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<?php foreach ( $rotations as $rotation ) : ?>
|
||||
<?php
|
||||
printf(
|
||||
'<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>',
|
||||
esc_url( add_query_arg( 'file_id', $rotation->get_file_id(), $rotation_url_base ) ),
|
||||
$file->get_file_id() === $rotation->get_file_id() ? 'primary' : 'secondary',
|
||||
absint( $rotation->get_rotation() )
|
||||
);
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<div class="wc-logs-single-file-actions">
|
||||
<?php
|
||||
// Download button.
|
||||
printf(
|
||||
'<a href="%1$s" class="button button-secondary">%2$s</a>',
|
||||
esc_url( $download_url ),
|
||||
esc_html__( 'Download', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
<?php
|
||||
// Delete button.
|
||||
printf(
|
||||
'<a href="%1$s" class="button button-secondary" onclick="%2$s">%3$s</a>',
|
||||
esc_url( $delete_url ),
|
||||
esc_attr( $delete_confirmation_js ),
|
||||
esc_html__( 'Delete permanently', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</header>
|
||||
<section id="logs-entries" class="wc-logs-entries">
|
||||
<?php while ( ! feof( $stream ) ) : ?>
|
||||
<?php
|
||||
$line = fgets( $stream );
|
||||
if ( is_string( $line ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_line does the escaping.
|
||||
echo $this->format_line( $line, $line_number );
|
||||
$line_number ++;
|
||||
}
|
||||
?>
|
||||
<?php endwhile; ?>
|
||||
</section>
|
||||
<script>
|
||||
// Clear the line number hash and highlight with a click.
|
||||
document.documentElement.addEventListener( 'click', ( event ) => {
|
||||
if ( window.location.hash && ! event.target.classList.contains( 'line-anchor' ) ) {
|
||||
let scrollPos = document.documentElement.scrollTop;
|
||||
window.location.hash = '';
|
||||
document.documentElement.scrollTop = scrollPos;
|
||||
history.replaceState( null, '', window.location.pathname + window.location.search );
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the search results view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_results_view(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2><?php esc_html_e( 'Search results', 'woocommerce' ); ?></h2>
|
||||
<?php $this->render_search_field(); ?>
|
||||
</header>
|
||||
<?php $list_table->display(); ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default values for URL query params for FileV2 views.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_query_param_defaults(): array {
|
||||
return array(
|
||||
'file_id' => '',
|
||||
'order' => $this->file_controller::DEFAULTS_GET_FILES['order'],
|
||||
'orderby' => $this->file_controller::DEFAULTS_GET_FILES['orderby'],
|
||||
'search' => '',
|
||||
'source' => $this->file_controller::DEFAULTS_GET_FILES['source'],
|
||||
'view' => 'list_files',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and validate URL query params for FileV2 views.
|
||||
*
|
||||
* @param array $param_keys Optional. The names of the params you want to get.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_query_params( array $param_keys = array() ): array {
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$params = filter_input_array(
|
||||
INPUT_GET,
|
||||
array(
|
||||
'file_id' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $file_id ) {
|
||||
return sanitize_file_name( wp_unslash( $file_id ) );
|
||||
},
|
||||
),
|
||||
'order' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(asc|desc)$/i',
|
||||
'default' => $defaults['order'],
|
||||
),
|
||||
),
|
||||
'orderby' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(created|modified|source|size)$/',
|
||||
'default' => $defaults['orderby'],
|
||||
),
|
||||
),
|
||||
'search' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $search ) {
|
||||
return esc_html( wp_unslash( $search ) );
|
||||
},
|
||||
),
|
||||
'source' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $source ) {
|
||||
return File::sanitize_source( wp_unslash( $source ) );
|
||||
},
|
||||
),
|
||||
'view' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(list_files|single_file|search_results|settings)$/',
|
||||
'default' => $defaults['view'],
|
||||
),
|
||||
),
|
||||
),
|
||||
false
|
||||
);
|
||||
$params = wp_parse_args( $params, $defaults );
|
||||
|
||||
if ( count( $param_keys ) > 0 ) {
|
||||
$params = array_intersect_key( $params, array_flip( $param_keys ) );
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and cache an instance of the list table.
|
||||
*
|
||||
* @param string $view The current view, which determines which list table class to get.
|
||||
*
|
||||
* @return FileListTable|SearchListTable
|
||||
*/
|
||||
private function get_list_table( string $view ) {
|
||||
if ( $this->list_table instanceof WP_List_Table ) {
|
||||
return $this->list_table;
|
||||
}
|
||||
|
||||
switch ( $view ) {
|
||||
case 'list_files':
|
||||
$this->list_table = new FileListTable( $this->file_controller, $this );
|
||||
break;
|
||||
case 'search_results':
|
||||
$this->list_table = new SearchListTable( $this->file_controller, $this );
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->list_table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register screen options for the logging views.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_screen_options( string $view ): void {
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$list_table = null;
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
if ( in_array( $view, array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $view );
|
||||
}
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
$list_table = WC_Admin_Status::get_db_log_list_table();
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $list_table instanceof WP_List_Table ) {
|
||||
// Ensure list table columns are initialized early enough to enable column hiding, if available.
|
||||
$list_table->prepare_column_headers();
|
||||
|
||||
add_screen_option(
|
||||
'per_page',
|
||||
array(
|
||||
'default' => $list_table->get_per_page_default(),
|
||||
'option' => $list_table::PER_PAGE_USER_OPTION_KEY,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process bulk actions initiated from the log file list table.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_list_table_bulk_actions( string $view ): void {
|
||||
// Bail if we're not using the file handler.
|
||||
if ( LogHandlerFileV2::class !== $this->settings->get_default_handler() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = $this->get_query_params( array( 'file_id' ) );
|
||||
|
||||
// Bail if this is not the list table view.
|
||||
if ( 'list_files' !== $view ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->get_list_table( $view )->current_action();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : $this->get_logs_tab_url();
|
||||
|
||||
if ( $action ) {
|
||||
check_admin_referer( 'bulk-log-files' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to manage log files.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$sendback = remove_query_arg( array( 'deleted' ), wp_get_referer() );
|
||||
|
||||
// Multiple file_id[] params will be filtered separately, but assigned to $files as an array.
|
||||
$file_ids = $params['file_id'];
|
||||
|
||||
if ( ! is_array( $file_ids ) || count( $file_ids ) < 1 ) {
|
||||
wp_safe_redirect( $sendback );
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ( $action ) {
|
||||
case 'export':
|
||||
if ( 1 === count( $file_ids ) ) {
|
||||
$export_error = $this->file_controller->export_single_file( reset( $file_ids ) );
|
||||
} else {
|
||||
$export_error = $this->file_controller->export_multiple_files( $file_ids );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $export_error ) ) {
|
||||
wp_die( wp_kses_post( $export_error ) );
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
$sendback = add_query_arg( 'deleted', $deleted, $sendback );
|
||||
|
||||
/**
|
||||
* If the delete action was triggered on the single file view, don't redirect back there
|
||||
* since the file doesn't exist anymore.
|
||||
*/
|
||||
$sendback = remove_query_arg( array( 'view', 'file_id' ), $sendback );
|
||||
break;
|
||||
}
|
||||
|
||||
$sendback = remove_query_arg( array( 'action', 'action2' ), $sendback );
|
||||
|
||||
wp_safe_redirect( $sendback );
|
||||
exit;
|
||||
} elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
|
||||
$removable_args = array( '_wp_http_referer', '_wpnonce', 'action', 'action2', 'filter_action' );
|
||||
wp_safe_redirect( remove_query_arg( $removable_args, $request_uri ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$deleted = filter_input( INPUT_GET, 'deleted', FILTER_VALIDATE_INT );
|
||||
|
||||
if ( is_numeric( $deleted ) ) {
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function() use ( $deleted ) {
|
||||
?>
|
||||
<div class="notice notice-info is-dismissible">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a number of files.
|
||||
esc_html( _n( '%s log file deleted.', '%s log files deleted.', $deleted, 'woocommerce' ) ),
|
||||
esc_html( number_format_i18n( $deleted ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a log file line.
|
||||
*
|
||||
* @param string $line The unformatted log file line.
|
||||
* @param int $line_number The line number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_line( string $line, int $line_number ): string {
|
||||
$classes = array( 'line' );
|
||||
|
||||
$line = esc_html( $line );
|
||||
if ( empty( $line ) ) {
|
||||
$line = ' ';
|
||||
}
|
||||
|
||||
$segments = explode( ' ', $line, 3 );
|
||||
$has_timestamp = false;
|
||||
$has_level = false;
|
||||
|
||||
if ( isset( $segments[0] ) && false !== strtotime( $segments[0] ) ) {
|
||||
$classes[] = 'log-entry';
|
||||
$segments[0] = sprintf(
|
||||
'<span class="log-timestamp">%s</span>',
|
||||
$segments[0]
|
||||
);
|
||||
$has_timestamp = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[1] ) && WC_Log_Levels::is_valid_level( strtolower( $segments[1] ) ) ) {
|
||||
$segments[1] = sprintf(
|
||||
'<span class="%1$s">%2$s</span>',
|
||||
esc_attr( 'log-level log-level--' . strtolower( $segments[1] ) ),
|
||||
esc_html( WC_Log_Levels::get_level_label( strtolower( $segments[1] ) ) )
|
||||
);
|
||||
$has_level = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[2] ) && $has_timestamp && $has_level ) {
|
||||
$message_chunks = explode( 'CONTEXT:', $segments[2], 2 );
|
||||
if ( isset( $message_chunks[1] ) ) {
|
||||
try {
|
||||
$maybe_json = stripslashes( html_entity_decode( trim( $message_chunks[1] ) ) );
|
||||
$context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR );
|
||||
|
||||
$message_chunks[1] = sprintf(
|
||||
'<details><summary>%1$s</summary>%2$s</details>',
|
||||
esc_html__( 'Additional context', 'woocommerce' ),
|
||||
wp_json_encode( $context, JSON_PRETTY_PRINT )
|
||||
);
|
||||
|
||||
$segments[2] = implode( ' ', $message_chunks );
|
||||
$classes[] = 'has-context';
|
||||
} catch ( \JsonException $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
// It's not valid JSON so don't do anything with it.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $segments ) > 1 ) {
|
||||
$line = implode( ' ', $segments );
|
||||
}
|
||||
|
||||
$classes = implode( ' ', $classes );
|
||||
|
||||
return sprintf(
|
||||
'<span id="L%1$d" class="%2$s">%3$s%4$s</span>',
|
||||
absint( $line_number ),
|
||||
esc_attr( $classes ),
|
||||
sprintf(
|
||||
'<a href="#L%1$d" class="line-anchor"></a>',
|
||||
absint( $line_number )
|
||||
),
|
||||
sprintf(
|
||||
'<span class="line-content">%s</span>',
|
||||
wp_kses_post( $line )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a form for searching within log files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_field(): void {
|
||||
$params = $this->get_query_params( array( 'date_end', 'date_filter', 'date_start', 'search', 'source' ) );
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$file_count = $this->file_controller->get_files( $params, true );
|
||||
|
||||
if ( $file_count > 0 ) {
|
||||
?>
|
||||
<form id="logs-search" class="wc-logs-search" method="get">
|
||||
<fieldset class="wc-logs-search-fieldset">
|
||||
<input type="hidden" name="page" value="wc-status" />
|
||||
<input type="hidden" name="tab" value="logs" />
|
||||
<input type="hidden" name="view" value="search_results" />
|
||||
<?php foreach ( $params as $key => $value ) : ?>
|
||||
<?php if ( $value !== $defaults[ $key ] ) : ?>
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $value ); ?>"
|
||||
/>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<label for="logs-search-field">
|
||||
<?php esc_html_e( 'Search within these files', 'woocommerce' ); ?>
|
||||
<input
|
||||
id="logs-search-field"
|
||||
class="wc-logs-search-field"
|
||||
type="text"
|
||||
name="search"
|
||||
value="<?php echo esc_attr( $params['search'] ); ?>"
|
||||
/>
|
||||
</label>
|
||||
<?php submit_button( __( 'Search', 'woocommerce' ), 'secondary', null, false ); ?>
|
||||
</fieldset>
|
||||
<?php if ( $file_count >= $this->file_controller::SEARCH_MAX_FILES ) : ?>
|
||||
<div class="wc-logs-search-notice">
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a number.
|
||||
esc_html__(
|
||||
'⚠️ Only %s files can be searched at one time. Try filtering the file list before searching.',
|
||||
'woocommerce'
|
||||
),
|
||||
esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_FILES ) )
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\File;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use WC_Admin_Settings;
|
||||
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
|
||||
use WP_Filesystem_Base;
|
||||
|
||||
/**
|
||||
* Settings class.
|
||||
*/
|
||||
class Settings {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Default values for logging settings.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
private const DEFAULTS = array(
|
||||
'logging_enabled' => true,
|
||||
'default_handler' => LogHandlerFileV2::class,
|
||||
'retention_period_days' => 30,
|
||||
'level_threshold' => 'none',
|
||||
);
|
||||
|
||||
/**
|
||||
* The prefix for settings keys used in the options table.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const PREFIX = 'woocommerce_logs_';
|
||||
|
||||
/**
|
||||
* Class Settings.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'save_settings' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for storing log files.
|
||||
*
|
||||
* The `wp_upload_dir` function takes into account the possibility of multisite, and handles changing
|
||||
* the directory if the context is switched to a different site in the network mid-request.
|
||||
*
|
||||
* @return string The full directory path, with trailing slash.
|
||||
*/
|
||||
public static function get_log_directory(): string {
|
||||
if ( true === Constants::get_constant( 'WC_LOG_DIR_CUSTOM' ) ) {
|
||||
$dir = Constants::get_constant( 'WC_LOG_DIR' );
|
||||
} else {
|
||||
$upload_dir = wc_get_container()->get( LegacyProxy::class )->call_function( 'wp_upload_dir' );
|
||||
|
||||
/**
|
||||
* Filter to change the directory for storing WooCommerce's log files.
|
||||
*
|
||||
* @param string $dir The full directory path, with trailing slash.
|
||||
*
|
||||
* @since 8.8.0
|
||||
*/
|
||||
$dir = apply_filters( 'woocommerce_log_directory', $upload_dir['basedir'] . '/wc-logs/' );
|
||||
}
|
||||
|
||||
$dir = trailingslashit( $dir );
|
||||
|
||||
$realpath = realpath( $dir );
|
||||
if ( false === $realpath ) {
|
||||
$result = wp_mkdir_p( $dir );
|
||||
|
||||
if ( true === $result ) {
|
||||
// Create infrastructure to prevent listing contents of the logs directory.
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$wp_filesystem->put_contents( $dir . '.htaccess', 'deny from all' );
|
||||
$wp_filesystem->put_contents( $dir . 'index.html', '' );
|
||||
}
|
||||
}
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render and save settings controls.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_settings_definitions(): array {
|
||||
$settings = array(
|
||||
'start' => array(
|
||||
'title' => __( 'Logs settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'logging_enabled' => array(
|
||||
'title' => __( 'Logger', 'woocommerce' ),
|
||||
'desc' => __( 'Enable logging', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'logging_enabled',
|
||||
'type' => 'checkbox',
|
||||
'value' => $this->logging_is_enabled() ? 'yes' : 'no',
|
||||
'default' => self::DEFAULTS['logging_enabled'] ? 'yes' : 'no',
|
||||
'autoload' => false,
|
||||
),
|
||||
'default_handler' => array(),
|
||||
'retention_period_days' => array(),
|
||||
'level_threshold' => array(),
|
||||
'end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
|
||||
if ( true === $this->logging_is_enabled() ) {
|
||||
$settings['default_handler'] = $this->get_default_handler_setting_definition();
|
||||
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
|
||||
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
|
||||
|
||||
$default_handler = $this->get_default_handler();
|
||||
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
|
||||
$settings += $this->get_filesystem_settings_definitions();
|
||||
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
|
||||
$settings += $this->get_database_settings_definitions();
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the default_handler setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_default_handler_setting_definition(): array {
|
||||
$handler_options = array(
|
||||
LogHandlerFileV2::class => __( 'File system (default)', 'woocommerce' ),
|
||||
WC_Log_Handler_DB::class => __( 'Database (not recommended on live sites)', 'woocommerce' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the list of logging handlers that can be set as the default handler.
|
||||
*
|
||||
* @param array $handler_options An associative array of class_name => description.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$handler_options = apply_filters( 'woocommerce_logger_handler_options', $handler_options );
|
||||
|
||||
$current_value = $this->get_default_handler();
|
||||
if ( ! array_key_exists( $current_value, $handler_options ) ) {
|
||||
$handler_options[ $current_value ] = $current_value;
|
||||
}
|
||||
|
||||
$desc = array();
|
||||
|
||||
$desc[] = __( 'Note that if this setting is changed, any log entries that have already been recorded will remain stored in their current location, but will not migrate.', 'woocommerce' );
|
||||
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_HANDLER' ) );
|
||||
if ( $hardcoded ) {
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a code variable.
|
||||
__( 'This setting cannot be changed here because it is defined in the %s constant.', 'woocommerce' ),
|
||||
'<code>WC_LOG_HANDLER</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Log storage', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This determines where log entries are saved.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'default_handler',
|
||||
'type' => 'radio',
|
||||
'value' => $current_value,
|
||||
'default' => self::DEFAULTS['default_handler'],
|
||||
'autoload' => false,
|
||||
'options' => $handler_options,
|
||||
'disabled' => $hardcoded ? array_keys( $handler_options ) : array(),
|
||||
'desc' => implode( '<br><br>', $desc ),
|
||||
'desc_at_end' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the retention_period_days setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_retention_period_days_setting_definition(): array {
|
||||
$custom_attributes = array(
|
||||
'min' => 1,
|
||||
'step' => 1,
|
||||
);
|
||||
|
||||
$desc = array();
|
||||
|
||||
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_days_to_retain_logs</code>'
|
||||
);
|
||||
}
|
||||
|
||||
$file_delete_has_filter = LogHandlerFileV2::class === $this->get_default_handler() && has_filter( 'woocommerce_logger_delete_expired_file' );
|
||||
if ( $file_delete_has_filter ) {
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'The %s hook has a filter set, so some log files may have different retention settings.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_delete_expired_file</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Retention period', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'retention_period_days',
|
||||
'type' => 'number',
|
||||
'value' => $this->get_retention_period(),
|
||||
'default' => self::DEFAULTS['retention_period_days'],
|
||||
'autoload' => false,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:70px;',
|
||||
'row_class' => 'logs-retention-period-days',
|
||||
'suffix' => sprintf(
|
||||
' %s',
|
||||
__( 'days', 'woocommerce' ),
|
||||
),
|
||||
'desc' => implode( '<br><br>', $desc ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the level_threshold setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_level_threshold_setting_definition(): array {
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_THRESHOLD' ) );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$desc = sprintf(
|
||||
// translators: %1$s is the name of a code variable. %2$s is the name of a file.
|
||||
__( 'This setting cannot be changed here because it is defined in the %1$s constant, probably in your %2$s file.', 'woocommerce' ),
|
||||
'<code>WC_LOG_THRESHOLD</code>',
|
||||
'<b>wp-config.php</b>'
|
||||
);
|
||||
}
|
||||
|
||||
$labels = WC_Log_Levels::get_all_level_labels();
|
||||
$labels['none'] = __( 'None', 'woocommerce' );
|
||||
|
||||
$custom_attributes = array();
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Level threshold', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets the minimum severity level of logs that will be stored. Lower severity levels will be ignored. "None" means all logs will be stored.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'level_threshold',
|
||||
'type' => 'select',
|
||||
'value' => $this->get_level_threshold(),
|
||||
'default' => self::DEFAULTS['level_threshold'],
|
||||
'autoload' => false,
|
||||
'options' => $labels,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:auto;',
|
||||
'desc' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to filesystem log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_filesystem_settings_definitions(): array {
|
||||
$location_info = array();
|
||||
$directory = self::get_log_directory();
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log files are stored in this directory: %s', 'woocommerce' ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $directory )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! wp_is_writable( $directory ) ) {
|
||||
$location_info[] = __( '⚠️ This directory does not appear to be writable.', 'woocommerce' );
|
||||
}
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is an amount of computer disk space, e.g. 5 KB.
|
||||
__( 'Directory size: %s', 'woocommerce' ),
|
||||
size_format( wc_get_container()->get( FileController::class )->get_log_directory_size() )
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'File system settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'log_directory' => array(
|
||||
'title' => __( 'Location', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'text' => implode( "\n\n", $location_info ),
|
||||
),
|
||||
'entry_format' => array(),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to database log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_database_settings_definitions(): array {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}woocommerce_log";
|
||||
|
||||
$location_info = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
|
||||
"<code>$table</code>"
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'Database settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'database_table' => array(
|
||||
'title' => __( 'Location', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'text' => $location_info,
|
||||
),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission of the settings form and update the settings values.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_settings( string $view ): void {
|
||||
$is_saving = 'settings' === $view && isset( $_POST['save_settings'] );
|
||||
|
||||
if ( $is_saving ) {
|
||||
check_admin_referer( self::PREFIX . 'settings' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to manage logging settings.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
WC_Admin_Settings::save_fields( $settings );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_form(): void {
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
?>
|
||||
<form id="mainform" class="wc-logs-settings" method="post">
|
||||
<?php WC_Admin_Settings::output_fields( $settings ); ?>
|
||||
<?php
|
||||
/**
|
||||
* Action fires after the built-in logging settings controls have been rendered.
|
||||
*
|
||||
* This is intended as a way to allow other logging settings controls to be added by extensions.
|
||||
*
|
||||
* @param bool $enabled True if logging is currently enabled.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_settings_form_fields', $this->logging_is_enabled() );
|
||||
?>
|
||||
<?php wp_nonce_field( self::PREFIX . 'settings' ); ?>
|
||||
<?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'save_settings' ); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the logging_enabled setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function logging_is_enabled(): bool {
|
||||
$key = self::PREFIX . 'logging_enabled';
|
||||
|
||||
$enabled = WC_Admin_Settings::get_option( $key, self::DEFAULTS['logging_enabled'] );
|
||||
$enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
|
||||
|
||||
if ( is_null( $enabled ) ) {
|
||||
$enabled = self::DEFAULTS['logging_enabled'];
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the default_handler setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_handler(): string {
|
||||
$key = self::PREFIX . 'default_handler';
|
||||
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) ) {
|
||||
$handler = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! class_exists( $handler ) || ! is_a( $handler, 'WC_Log_Handler_Interface', true ) ) {
|
||||
$handler = self::DEFAULTS['default_handler'];
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the retention_period_days setting.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_retention_period(): int {
|
||||
$key = self::PREFIX . 'retention_period_days';
|
||||
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
|
||||
if ( has_filter( 'woocommerce_logger_days_to_retain_logs' ) ) {
|
||||
/**
|
||||
* Filter the retention period of log entries.
|
||||
*
|
||||
* @param int $days The number of days to retain log entries.
|
||||
*
|
||||
* @since 3.4.0
|
||||
*/
|
||||
$retention_period = apply_filters( 'woocommerce_logger_days_to_retain_logs', $retention_period );
|
||||
} else {
|
||||
$retention_period = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
$retention_period = absint( $retention_period );
|
||||
|
||||
if ( $retention_period < 1 ) {
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
}
|
||||
|
||||
return $retention_period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the level_threshold setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_level_threshold(): string {
|
||||
$key = self::PREFIX . 'level_threshold';
|
||||
|
||||
$threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' );
|
||||
|
||||
if ( is_null( $threshold ) ) {
|
||||
$threshold = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! WC_Log_Levels::is_valid_level( $threshold ) ) {
|
||||
$threshold = self::DEFAULTS['level_threshold'];
|
||||
}
|
||||
|
||||
return $threshold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Marketing.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Marketing feature.
|
||||
*/
|
||||
class Marketing {
|
||||
|
||||
use CouponsMovedTrait;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu name value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_NAME_KEY = 0;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu location value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_LOCATION_KEY = 2;
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Marketing instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 5 );
|
||||
add_action( 'admin_menu', array( $this, 'add_parent_menu_item' ), 6 );
|
||||
|
||||
// Overwrite submenu default ordering for marketing menu. High priority gives plugins the chance to register their own menu items.
|
||||
add_action( 'admin_menu', array( $this, 'reorder_marketing_submenu' ), 99 );
|
||||
|
||||
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add main marketing menu item.
|
||||
*
|
||||
* Uses priority of 9 so other items can easily be added at the default priority (10).
|
||||
*/
|
||||
public function add_parent_menu_item() {
|
||||
if ( ! Features::is_enabled( 'navigation' ) ) {
|
||||
add_menu_page(
|
||||
__( 'Marketing', 'woocommerce' ),
|
||||
__( 'Marketing', 'woocommerce' ),
|
||||
'manage_woocommerce',
|
||||
'woocommerce-marketing',
|
||||
null,
|
||||
'dashicons-megaphone',
|
||||
58
|
||||
);
|
||||
}
|
||||
|
||||
PageController::get_instance()->connect_page(
|
||||
[
|
||||
'id' => 'woocommerce-marketing',
|
||||
'title' => 'Marketing',
|
||||
'capability' => 'manage_woocommerce',
|
||||
'path' => 'wc-admin&path=/marketing',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers report pages.
|
||||
*/
|
||||
public function register_pages() {
|
||||
$this->register_overview_page();
|
||||
|
||||
$controller = PageController::get_instance();
|
||||
$defaults = [
|
||||
'parent' => 'woocommerce-marketing',
|
||||
'existing_page' => false,
|
||||
];
|
||||
|
||||
$marketing_pages = apply_filters( 'woocommerce_marketing_menu_items', [] );
|
||||
foreach ( $marketing_pages as $marketing_page ) {
|
||||
if ( ! is_array( $marketing_page ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$marketing_page = array_merge( $defaults, $marketing_page );
|
||||
|
||||
if ( $marketing_page['existing_page'] ) {
|
||||
$controller->connect_page( $marketing_page );
|
||||
} else {
|
||||
$controller->register_page( $marketing_page );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the main Marketing page, which is Marketing > Overview.
|
||||
*
|
||||
* This is done separately because we need to ensure the page is registered properly and
|
||||
* that the link is done properly. For some reason the normal page registration process
|
||||
* gives us the wrong menu link.
|
||||
*/
|
||||
protected function register_overview_page() {
|
||||
global $submenu;
|
||||
|
||||
// First register the page.
|
||||
PageController::get_instance()->register_page(
|
||||
[
|
||||
'id' => 'woocommerce-marketing-overview',
|
||||
'title' => __( 'Overview', 'woocommerce' ),
|
||||
'path' => 'wc-admin&path=/marketing',
|
||||
'parent' => 'woocommerce-marketing',
|
||||
'nav_args' => array(
|
||||
'parent' => 'woocommerce-marketing',
|
||||
'order' => 10,
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
// Now fix the path, since register_page() gets it wrong.
|
||||
if ( ! isset( $submenu['woocommerce-marketing'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $submenu['woocommerce-marketing'] as &$item ) {
|
||||
// The "slug" (aka the path) is the third item in the array.
|
||||
if ( 0 === strpos( $item[2], 'wc-admin' ) ) {
|
||||
$item[2] = 'admin.php?page=' . $item[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order marketing menu items alphabeticaly.
|
||||
* Overview should be first, and Coupons should be second, followed by other marketing menu items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reorder_marketing_submenu() {
|
||||
global $submenu;
|
||||
|
||||
if ( ! isset( $submenu['woocommerce-marketing'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$marketing_submenu = $submenu['woocommerce-marketing'];
|
||||
$new_menu_order = array();
|
||||
|
||||
// Overview should be first.
|
||||
$overview_key = array_search( 'Overview', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $overview_key ) {
|
||||
/*
|
||||
* If Overview is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the overview page by its path.
|
||||
*/
|
||||
$overview_key = array_search( 'admin.php?page=wc-admin&path=/marketing', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $overview_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $overview_key ];
|
||||
array_splice( $marketing_submenu, $overview_key, 1 );
|
||||
}
|
||||
|
||||
// Coupons should be second.
|
||||
$coupons_key = array_search( 'Coupons', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $coupons_key ) {
|
||||
/*
|
||||
* If Coupons is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the coupons page by its path.
|
||||
*/
|
||||
$coupons_key = array_search( 'edit.php?post_type=shop_coupon', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $coupons_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $coupons_key ];
|
||||
array_splice( $marketing_submenu, $coupons_key, 1 );
|
||||
}
|
||||
|
||||
// Sort the rest of the items alphabetically.
|
||||
usort(
|
||||
$marketing_submenu,
|
||||
function( $a, $b ) {
|
||||
return strcmp( $a[0], $b[0] );
|
||||
}
|
||||
);
|
||||
|
||||
$new_menu_order = array_merge( $new_menu_order, $marketing_submenu );
|
||||
|
||||
$submenu['woocommerce-marketing'] = $new_menu_order; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings for marketing feature.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
* @return array
|
||||
*/
|
||||
public function component_settings( $settings ) {
|
||||
// Bail early if not on a wc-admin powered page.
|
||||
if ( ! PageController::is_admin_page() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$settings['marketing']['installedExtensions'] = InstalledExtensions::get_data();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Marketing Specs Handler
|
||||
*
|
||||
* Fetches the specifications for the marketing feature from WooCommerce.com API.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Marketing;
|
||||
|
||||
/**
|
||||
* Marketing Specifications Class.
|
||||
*
|
||||
* @internal
|
||||
* @since x.x.x
|
||||
*/
|
||||
class MarketingSpecs {
|
||||
/**
|
||||
* Name of knowledge base post transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base';
|
||||
|
||||
/**
|
||||
* Load knowledge base posts from WooCommerce.com
|
||||
*
|
||||
* @param string|null $topic The topic of marketing knowledgebase to retrieve.
|
||||
* @return array
|
||||
*/
|
||||
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(
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
),
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-knowledgebase/v1/posts/' . $topic
|
||||
);
|
||||
|
||||
$request = wp_remote_get(
|
||||
$request_url,
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$posts = array();
|
||||
|
||||
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
|
||||
$raw_posts = json_decode( $request['body'], true );
|
||||
|
||||
foreach ( $raw_posts as $raw_post ) {
|
||||
$post = array(
|
||||
'title' => html_entity_decode( $raw_post['title']['rendered'] ),
|
||||
'date' => $raw_post['date_gmt'],
|
||||
'link' => $raw_post['link'],
|
||||
'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '',
|
||||
'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '',
|
||||
);
|
||||
|
||||
$featured_media = isset( $raw_post['_embedded']['wp:featuredmedia'] ) && is_array( $raw_post['_embedded']['wp:featuredmedia'] ) ? $raw_post['_embedded']['wp:featuredmedia'] : array();
|
||||
if ( count( $featured_media ) > 0 ) {
|
||||
$image = current( $featured_media );
|
||||
$post['image'] = add_query_arg(
|
||||
array(
|
||||
'resize' => '650,340',
|
||||
'crop' => 1,
|
||||
),
|
||||
$image['source_url']
|
||||
);
|
||||
}
|
||||
|
||||
$posts[] = $post;
|
||||
}
|
||||
}
|
||||
|
||||
set_transient(
|
||||
$kb_transient,
|
||||
$posts,
|
||||
// Expire transient in 15 minutes if remote get failed.
|
||||
empty( $posts ) ? 900 : DAY_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Marketplace.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use WC_Helper_Updater;
|
||||
use WC_Woo_Update_Manager_Plugin;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Marketplace feature.
|
||||
*/
|
||||
class Marketplace {
|
||||
|
||||
const MARKETPLACE_TAB_SLUG = 'woo';
|
||||
|
||||
/**
|
||||
* Class initialization, to be executed when the class is resolved by the container.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final public function init() {
|
||||
if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
|
||||
/** Feature controller instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
$feature_controller->change_feature_enable( 'marketplace', true );
|
||||
}
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
// Add a Woo Marketplace link to the plugin install action links.
|
||||
add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) );
|
||||
add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) );
|
||||
add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers report pages.
|
||||
*/
|
||||
public function register_pages() {
|
||||
if ( ! function_exists( 'wc_admin_register_page' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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' ) . WC_Helper_Updater::get_updates_count_html(),
|
||||
'page_title' => __( 'Extensions', 'woocommerce' ),
|
||||
'path' => '/extensions',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The marketplace items used in the menu.
|
||||
*
|
||||
* @since 8.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue update script.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page.
|
||||
*/
|
||||
public function enqueue_scripts( $hook_suffix ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
if ( 'woocommerce_page_wc-admin' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $_GET['path'] ) || '/extensions' !== $_GET['path'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue WordPress updates script to enable plugin and theme installs and updates.
|
||||
wp_enqueue_script( 'updates' );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Woo Marketplace link to the plugin install action links.
|
||||
*
|
||||
* @param array $tabs Plugins list tabs.
|
||||
* @return array
|
||||
*/
|
||||
public function add_woo_plugin_install_action_link( $tabs ) {
|
||||
$tabs[ self::MARKETPLACE_TAB_SLUG ] = 'Woo';
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Woo tab when the user clicks on the Woo link in the plugin installer.
|
||||
*/
|
||||
public function maybe_open_woo_tab() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['tab'] ) || self::MARKETPLACE_TAB_SLUG !== $_GET['tab'] ) {
|
||||
return;
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$woo_url = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-admin',
|
||||
'path' => '/extensions',
|
||||
'tab' => 'extensions',
|
||||
'ref' => 'plugins',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
wp_safe_redirect( $woo_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add styles to the plugin install page.
|
||||
*/
|
||||
public function add_plugins_page_styles() {
|
||||
?>
|
||||
<style>
|
||||
.plugin-install-woo > a::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23646970'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23646970'/%3E%3C/svg%3E%0A");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: text-top;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.plugin-install-woo:hover > a::after {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23135E96'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23135E96'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Determine if the mobile app banner shows on Android devices
|
||||
*/
|
||||
class MobileAppBanner {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Analytics instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store user preferences for the mobile app banner
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
*/
|
||||
public function add_user_data_fields( $user_data_fields ) {
|
||||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'android_app_banner_dismissed',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Add First Product.
|
||||
*
|
||||
* Adds a note (type `email`) to bring the client back to the store setup flow.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Add_First_Product.
|
||||
*/
|
||||
class AddFirstProduct {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-add-first-product-note';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
if ( ! self::wc_admin_active_for( 2 * DAY_IN_SECONDS ) || self::wc_admin_active_for( 5 * DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show if there is a product.
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'return' => 'ids',
|
||||
'status' => array( 'publish' ),
|
||||
)
|
||||
);
|
||||
$products = $query->get_products();
|
||||
if ( 0 !== count( $products ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show if there is an orders.
|
||||
$args = array(
|
||||
'limit' => 1,
|
||||
'return' => 'ids',
|
||||
);
|
||||
$orders = wc_get_orders( $args );
|
||||
if ( 0 !== count( $orders ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If you're updating the following please use sprintf to separate HTML tags.
|
||||
// https://github.com/woocommerce/woocommerce-admin/pull/6617#discussion_r596889685.
|
||||
$content_lines = array(
|
||||
'{greetings}<br/><br/>',
|
||||
/* translators: %s: line break */
|
||||
sprintf( __( 'Nice one; you\'ve created a WooCommerce store! Now it\'s time to add your first product and get ready to start selling.%s', 'woocommerce' ), '<br/><br/>' ),
|
||||
__( 'There are three ways to add your products: you can <strong>create products manually, import them at once via CSV file</strong>, or <strong>migrate them from another service</strong>.<br/><br/>', 'woocommerce' ),
|
||||
/* translators: %1$s is an open anchor tag (<a>) and %2$s is a close link tag (</a>). */
|
||||
sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '<a href="https://woocommerce.com/document/managing-products/?utm_source=help_panel&utm_medium=product">', '</a>' ),
|
||||
);
|
||||
|
||||
$additional_data = array(
|
||||
'role' => 'administrator',
|
||||
);
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Add your first product', 'woocommerce' ) );
|
||||
$note->set_content( implode( '', $content_lines ) );
|
||||
$note->set_content_data( (object) $additional_data );
|
||||
$note->set_image(
|
||||
plugins_url(
|
||||
'/images/admin_notes/dashboard-widget-setup.png',
|
||||
WC_ADMIN_PLUGIN_FILE
|
||||
)
|
||||
);
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_EMAIL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action( 'add-first-product', __( 'Add a product', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&task=products' ) );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin (Dashboard) choosing a theme note
|
||||
*
|
||||
* Adds notes to the merchant's inbox about choosing a theme.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Giving_Feedback_Notes
|
||||
*/
|
||||
class ChoosingTheme {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-choosing-a-theme';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// We need to show choosing a theme notification after 1 day of install.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create our new note.
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Choosing a theme?', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Check out the themes that are compatible with WooCommerce and choose one aligned with your brand and business needs.', 'woocommerce' ) );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'visit-the-theme-marketplace',
|
||||
__( 'Visit the theme marketplace', 'woocommerce' ),
|
||||
'https://woocommerce.com/product-category/themes/?utm_source=inbox&utm_medium=product'
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Coupon Page Moved provider.
|
||||
*
|
||||
* Adds a notice when the store manager access the coupons page via the old WooCommerce > Coupons menu.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait;
|
||||
use stdClass;
|
||||
use WC_Data_Store;
|
||||
|
||||
/**
|
||||
* Coupon_Page_Moved class.
|
||||
*/
|
||||
class CouponPageMoved {
|
||||
|
||||
use NoteTraits, CouponsMovedTrait;
|
||||
|
||||
const NOTE_NAME = 'wc-admin-coupon-page-moved';
|
||||
|
||||
/**
|
||||
* Initialize our hooks.
|
||||
*/
|
||||
public function init() {
|
||||
if ( ! wc_coupons_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_init', [ $this, 'possibly_add_note' ] );
|
||||
add_action( 'admin_init', [ $this, 'redirect_to_coupons' ] );
|
||||
add_action( 'woocommerce_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a note can and should be added.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_be_added() {
|
||||
if ( ! wc_coupons_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't add the notice if the legacy coupon menu is already disabled.
|
||||
if ( ! self::should_display_legacy_menu() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't add the notice if it's been hidden by the user before.
|
||||
if ( self::has_dismissed_note() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we already have a notice, don't add a new one.
|
||||
if ( self::has_unactioned_note() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset( $_GET[ self::$query_key ] ) && (bool) $_GET[ self::$query_key ]; // phpcs:ignore WordPress.Security.NonceVerification
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note object for this class.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Coupon management has moved!', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Coupons can now be managed from Marketing > Coupons. Click the button below to remove the legacy WooCommerce > Coupons menu item.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( new stdClass() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'remove-legacy-coupon-menu',
|
||||
__( 'Remove legacy coupon menu', 'woocommerce' ),
|
||||
wc_admin_url( '&action=remove-coupon-menu' ),
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find notes that have not been actioned.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function has_unactioned_note() {
|
||||
$note = Notes::get_note_by_name( self::NOTE_NAME );
|
||||
|
||||
if ( ! $note ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $note->get_status() === 'unactioned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any notes have been dismissed by the user previously.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function has_dismissed_note() {
|
||||
$note = Notes::get_note_by_name( self::NOTE_NAME );
|
||||
|
||||
if ( ! $note ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $note->get_is_deleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data store object.
|
||||
*
|
||||
* @return DataStore The data store object.
|
||||
*/
|
||||
protected static function get_data_store() {
|
||||
return WC_Data_Store::load( 'admin-note' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe redirect to the coupon page to force page refresh.
|
||||
*/
|
||||
public function redirect_to_coupons() {
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
if (
|
||||
! isset( $_GET['page'] ) ||
|
||||
'wc-admin' !== $_GET['page'] ||
|
||||
! isset( $_GET['action'] ) ||
|
||||
'remove-coupon-menu' !== $_GET['action'] ||
|
||||
! defined( 'WC_ADMIN_PLUGIN_FILE' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
/* phpcs:enable */
|
||||
$this->display_legacy_menu( false );
|
||||
wp_safe_redirect( self::get_management_url( 'coupons' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable legacy coupon menu when installing for the first time.
|
||||
*/
|
||||
public function disable_legacy_menu_for_new_install() {
|
||||
$this->display_legacy_menu( false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Customize your online store with WooCommerce blocks.
|
||||
*
|
||||
* Adds a note to customize the client online store with WooCommerce blocks.
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Customize_Store_With_Blocks.
|
||||
*/
|
||||
class CustomizeStoreWithBlocks {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-customize-store-with-blocks';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// We want to show the note after fourteen days.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 14 * DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show if there aren't products.
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'return' => 'ids',
|
||||
'status' => array( 'publish' ),
|
||||
)
|
||||
);
|
||||
$products = $query->get_products();
|
||||
if ( 0 === count( $products ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Customize your online store with WooCommerce blocks', 'woocommerce' ) );
|
||||
$note->set_content( __( 'With our blocks, you can select and display products, categories, filters, and more virtually anywhere on your site — no need to use shortcodes or edit lines of code. Learn more about how to use each one of them.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'customize-store-with-blocks',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/posts/how-to-customize-your-online-store-with-woocommerce-blocks/?utm_source=inbox&utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: How to customize your product catalog note provider
|
||||
*
|
||||
* Adds a note with a link to the customizer a day after adding the first product
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Class CustomizingProductCatalog
|
||||
*
|
||||
* @package Automattic\WooCommerce\Admin\Notes
|
||||
*/
|
||||
class CustomizingProductCatalog {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-customizing-product-catalog';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'paginate' => true,
|
||||
'status' => array( 'publish' ),
|
||||
'orderby' => 'post_date',
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
$products = $query->get_products();
|
||||
|
||||
// we need at least 1 product.
|
||||
if ( 0 === $products->total ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product = $products->products[0];
|
||||
$created_timestamp = $product->get_date_created()->getTimestamp();
|
||||
$is_a_day_old = ( time() - $created_timestamp ) >= DAY_IN_SECONDS;
|
||||
|
||||
// the product must be at least 1 day old.
|
||||
if ( ! $is_a_day_old ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// store must not been active more than 14 days.
|
||||
if ( self::wc_admin_active_for( DAY_IN_SECONDS * 14 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'How to customize your product catalog', 'woocommerce' ) );
|
||||
$note->set_content( __( 'You want your product catalog and images to look great and align with your brand. This guide will give you all the tips you need to get your products looking great in your store.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'day-after-first-product',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: EU VAT Number Note.
|
||||
*
|
||||
* Adds a note for EU store to install the EU VAT Number extension.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* EU_VAT_Number
|
||||
*/
|
||||
class EUVATNumber {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-eu-vat-number';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
if ( 'yes' !== get_option( 'wc_connect_taxes_enabled', 'no' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$country_code = WC()->countries->get_base_country();
|
||||
$eu_countries = WC()->countries->get_european_union_countries();
|
||||
if ( ! in_array( $country_code, $eu_countries, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( "If your store is based in the EU, we recommend using the EU VAT Number extension in addition to automated taxes. It provides your checkout with a field to collect and validate a customer's EU VAT number, if they have one.", 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Collect and validate EU VAT numbers at checkout', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/products/eu-vat-number/?utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Edit products on the move note.
|
||||
*
|
||||
* Adds a note to download the mobile app.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Edit_Products_On_The_Move
|
||||
*/
|
||||
class EditProductsOnTheMove {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-edit-products-on-the-move';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only add this note if this store is at least a year old.
|
||||
$year_in_seconds = 365 * DAY_IN_SECONDS;
|
||||
if ( ! self::wc_admin_active_for( $year_in_seconds ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the previous mobile app notes have not been actioned.
|
||||
if ( MobileApp::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( ManageOrdersOnTheGo::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( PerformanceOnMobile::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'Edit products on the move', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Edit and create new products from your mobile devices with the Woo app', 'woocommerce' ) );
|
||||
$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(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles emailing user notes.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
|
||||
/**
|
||||
* Include dependencies.
|
||||
*/
|
||||
if ( ! class_exists( 'WC_Email', false ) ) {
|
||||
include_once WC_ABSPATH . 'includes/emails/class-wc-email.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* EmailNotification Class.
|
||||
*/
|
||||
class EmailNotification extends \WC_Email {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Note $note The notification to send.
|
||||
*/
|
||||
public function __construct( $note ) {
|
||||
$this->note = $note;
|
||||
$this->id = 'merchant_notification';
|
||||
$this->template_base = WC_ADMIN_ABSPATH . 'includes/react-admin/emails/';
|
||||
$this->placeholders = array(
|
||||
'{greetings}' => __( 'Hi there,', 'woocommerce' ),
|
||||
);
|
||||
|
||||
// Call parent constructor.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* This email has no user-facing settings.
|
||||
*/
|
||||
public function init_form_fields() {}
|
||||
|
||||
/**
|
||||
* This email has no user-facing settings.
|
||||
*/
|
||||
public function init_settings() {}
|
||||
|
||||
/**
|
||||
* Return template filename.
|
||||
*
|
||||
* @param string $type Type of email to send.
|
||||
* @return string
|
||||
*/
|
||||
public function get_template_filename( $type = 'html' ) {
|
||||
if ( ! in_array( $type, array( 'html', 'plain' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
$content_data = $this->note->get_content_data();
|
||||
$template_filename = "{$type}-merchant-notification.php";
|
||||
if ( isset( $content_data->{"template_{$type}"} ) && file_exists( $this->template_base . $content_data->{ "template_{$type}" } ) ) {
|
||||
$template_filename = $content_data[ "template_{$type}" ];
|
||||
}
|
||||
return $template_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return email type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_email_type() {
|
||||
return class_exists( 'DOMDocument' ) ? 'html' : 'plain';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email heading.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_heading() {
|
||||
$content_data = $this->note->get_content_data();
|
||||
if ( isset( $content_data->heading ) ) {
|
||||
return $content_data->heading;
|
||||
}
|
||||
|
||||
return $this->note->get_title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email headers.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_headers() {
|
||||
$header = 'Content-Type: ' . $this->get_content_type() . "\r\n";
|
||||
return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email subject.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_subject() {
|
||||
return $this->note->get_title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get note content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_note_content() {
|
||||
return $this->note->get_content();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get note image.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_image() {
|
||||
return $this->note->get_image();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email action.
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
public function get_actions() {
|
||||
return $this->note->get_actions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content html.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_html() {
|
||||
return wc_get_template_html(
|
||||
$this->get_template_filename( 'html' ),
|
||||
array(
|
||||
'email_actions' => $this->get_actions(),
|
||||
'email_content' => $this->format_string( $this->get_note_content() ),
|
||||
'email_heading' => $this->format_string( $this->get_heading() ),
|
||||
'email_image' => $this->get_image(),
|
||||
'sent_to_admin' => true,
|
||||
'plain_text' => false,
|
||||
'email' => $this,
|
||||
'opened_tracking_url' => $this->opened_tracking_url,
|
||||
'trigger_note_action_url' => $this->trigger_note_action_url,
|
||||
),
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content plain.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_plain() {
|
||||
return wc_get_template_html(
|
||||
$this->get_template_filename( 'plain' ),
|
||||
array(
|
||||
'email_heading' => $this->format_string( $this->get_heading() ),
|
||||
'email_content' => $this->format_string( $this->get_note_content() ),
|
||||
'email_actions' => $this->get_actions(),
|
||||
'sent_to_admin' => true,
|
||||
'plain_text' => true,
|
||||
'email' => $this,
|
||||
'trigger_note_action_url' => $this->trigger_note_action_url,
|
||||
),
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the sending of this email.
|
||||
*
|
||||
* @param string $user_email Email to send the note.
|
||||
* @param int $user_id User id to to track the note.
|
||||
* @param string $user_name User's name.
|
||||
*/
|
||||
public function trigger( $user_email, $user_id, $user_name ) {
|
||||
$this->recipient = $user_email;
|
||||
$this->opened_tracking_url = sprintf(
|
||||
'%1$s/wp-json/wc-analytics/admin/notes/tracker/%2$d/user/%3$d',
|
||||
site_url(),
|
||||
$this->note->get_id(),
|
||||
$user_id
|
||||
);
|
||||
$this->trigger_note_action_url = sprintf(
|
||||
'%1$s&external_redirect=1¬e=%2$d&user=%3$d&action=',
|
||||
wc_admin_url(),
|
||||
$this->note->get_id(),
|
||||
$user_id
|
||||
);
|
||||
|
||||
if ( $user_name ) {
|
||||
/* translators: %s = merchant name */
|
||||
$this->placeholders['{greetings}'] = sprintf( __( 'Hi %s,', 'woocommerce' ), $user_name );
|
||||
}
|
||||
|
||||
$this->send(
|
||||
$this->get_recipient(),
|
||||
$this->get_subject(),
|
||||
$this->get_content(),
|
||||
$this->get_headers(),
|
||||
$this->get_attachments()
|
||||
);
|
||||
Notes::record_tracks_event_with_user( $user_id, 'email_note_sent', array( 'note_name' => $this->note->get_name() ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Do you need help with adding your first product?
|
||||
*
|
||||
* Adds a note to ask the client if they need help adding their first product.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* First_Product.
|
||||
*/
|
||||
class FirstProduct {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-first-product';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// We want to show the note after seven days.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// Don't show if there are products.
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'paginate' => true,
|
||||
'return' => 'ids',
|
||||
'status' => array( 'publish' ),
|
||||
)
|
||||
);
|
||||
$products = $query->get_products();
|
||||
$count = $products->total;
|
||||
if ( 0 !== $count ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Do you need help with adding your first product?', 'woocommerce' ) );
|
||||
$note->set_content( __( 'This video tutorial will help you go through the process of adding your first product in WooCommerce.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'first-product-watch-tutorial',
|
||||
__( 'Watch tutorial', 'woocommerce' ),
|
||||
'https://www.youtube.com/watch?v=sFtXa00Jf_o&list=PLHdG8zvZd0E575Ia8Mu3w1h750YLXNfsC&index=24'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin (Dashboard) Giving feedback notes provider
|
||||
*
|
||||
* Adds notes to the merchant's inbox about giving feedback.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\Survey;
|
||||
|
||||
/**
|
||||
* Giving_Feedback_Notes
|
||||
*/
|
||||
class GivingFeedbackNotes {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-store-notice-giving-feedback-2';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create our new note.
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'You\'re invited to share your experience', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Now that you’ve chosen us as a partner, our goal is to make sure we\'re providing the right tools to meet your needs. We\'re looking forward to having your feedback on the store setup experience so we can improve it in the future.', 'woocommerce' ) );
|
||||
$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(
|
||||
'share-feedback',
|
||||
__( 'Share feedback', 'woocommerce' ),
|
||||
Survey::get_url( '/store-setup-survey' )
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Add Install Jetpack and WooCommerce Shipping & Tax Plugin Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox prompting them to install the Jetpack
|
||||
* and WooCommerce Shipping & Tax plugins after it fails to install during
|
||||
* WooCommerce setup.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
|
||||
/**
|
||||
* Install_JP_And_WCS_Plugins
|
||||
*/
|
||||
class InstallJPAndWCSPlugins {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-install-jp-and-wcs-plugins';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_note_action_install-jp-and-wcs-plugins', array( $this, 'install_jp_and_wcs_plugins' ) );
|
||||
add_action( 'activated_plugin', array( $this, 'action_note' ) );
|
||||
add_action( 'woocommerce_plugins_install_api_error', array( $this, 'on_install_error' ) );
|
||||
add_action( 'woocommerce_plugins_install_error', array( $this, 'on_install_error' ) );
|
||||
add_action( 'woocommerce_plugins_activate_error', array( $this, 'on_install_error' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$content = __( 'We noticed that there was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again and enjoy all the advantages of having the plugins connected to your store! Sorry for the inconvenience. The "Jetpack" and "WooCommerce Shipping & Tax" plugins will be installed & activated for free.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Uh oh... There was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again.', '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(
|
||||
'install-jp-and-wcs-plugins',
|
||||
__( 'Install plugins', 'woocommerce' ),
|
||||
false,
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action the Install Jetpack and WooCommerce Shipping & Tax note, if any exists,
|
||||
* and as long as both the Jetpack and WooCommerce Shipping & Tax plugins have been
|
||||
* activated.
|
||||
*/
|
||||
public static function action_note() {
|
||||
// Make sure that both plugins are active before actioning the note.
|
||||
$active_plugin_slugs = PluginsHelper::get_active_plugin_slugs();
|
||||
$jp_active = in_array( 'jetpack', $active_plugin_slugs, true );
|
||||
$wcs_active = in_array( 'woocommerce-services', $active_plugin_slugs, true );
|
||||
|
||||
if ( ! $jp_active || ! $wcs_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Action any notes with a matching name.
|
||||
$data_store = Notes::load_data_store();
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
|
||||
foreach ( $note_ids as $note_id ) {
|
||||
$note = Notes::get_note( $note_id );
|
||||
|
||||
if ( $note ) {
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
$note->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the Jetpack and WooCommerce Shipping & Tax plugins in response to the action
|
||||
* being clicked in the admin note.
|
||||
*
|
||||
* @param Note $note The note being actioned.
|
||||
*/
|
||||
public function install_jp_and_wcs_plugins( $note ) {
|
||||
if ( self::NOTE_NAME !== $note->get_name() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->install_and_activate_plugin( 'jetpack' );
|
||||
$this->install_and_activate_plugin( 'woocommerce-services' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs and activates the specified plugin.
|
||||
*
|
||||
* @param string $plugin The plugin slug.
|
||||
*/
|
||||
private function install_and_activate_plugin( $plugin ) {
|
||||
$install_request = array( 'plugin' => $plugin );
|
||||
$installer = new \Automattic\WooCommerce\Admin\API\OnboardingPlugins();
|
||||
$result = $installer->install_plugin( $install_request );
|
||||
|
||||
// @todo Use the error statuses to decide whether or not to action the note.
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activate_request = array( 'plugins' => $plugin );
|
||||
|
||||
$installer->activate_plugins( $activate_request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alert notification in response to an error installing a plugin.
|
||||
*
|
||||
* @param string $slug The slug of the plugin being installed.
|
||||
*/
|
||||
public function on_install_error( $slug ) {
|
||||
// Exit early if we're not installing the Jetpack or the WooCommerce Shipping & Tax plugins.
|
||||
if ( 'jetpack' !== $slug && 'woocommerce-services' !== $slug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::possibly_add_note();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Launch Checklist Note.
|
||||
*
|
||||
* Adds a note to cover pre-launch checklist items for store owners.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Launch_Checklist
|
||||
*/
|
||||
class LaunchChecklist {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-launch-checklist';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only add this note if completing the task list or completed 3 tasks in 10 days.
|
||||
$completed_tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
|
||||
$ten_days_in_seconds = 10 * DAY_IN_SECONDS;
|
||||
if (
|
||||
! get_option( 'woocommerce_task_list_complete' ) &&
|
||||
(
|
||||
count( $completed_tasks ) < 3 ||
|
||||
self::is_wc_admin_active_in_date_range( 'week-1-4', $ten_days_in_seconds )
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( 'To make sure you never get that sinking "what did I forget" feeling, we\'ve put together the essential pre-launch checklist.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Ready to launch your store?', '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( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/posts/pre-launch-checklist-the-essentials/?utm_source=inbox&utm_medium=product' );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin note on how to migrate from Magento.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Onboarding;
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* MagentoMigration
|
||||
*/
|
||||
class MagentoMigration {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-magento-migration';
|
||||
|
||||
/**
|
||||
* Attach hooks.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( __CLASS__, 'possibly_add_note' ) );
|
||||
add_action( 'woocommerce_admin_magento_migration_note', array( __CLASS__, 'save_note' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the note if it passes predefined conditions.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
$onboarding_profile = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
|
||||
if ( empty( $onboarding_profile ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset( $onboarding_profile['other_platform'] ) ||
|
||||
'magento' !== $onboarding_profile['other_platform']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset( $onboarding_profile['setup_client'] ) ||
|
||||
$onboarding_profile['setup_client']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC()->queue()->schedule_single( time() + ( 5 * MINUTE_IN_SECONDS ), 'woocommerce_admin_magento_migration_note' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the note to the database.
|
||||
*/
|
||||
public static function save_note() {
|
||||
$note = self::get_note();
|
||||
|
||||
if ( self::note_exists() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'How to Migrate from Magento to WooCommerce', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Changing platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) );
|
||||
$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(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Manage orders on the go note.
|
||||
*
|
||||
* Adds a note to download the mobile app to manage orders on the go.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Manage_Orders_On_The_Go
|
||||
*/
|
||||
class ManageOrdersOnTheGo {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-manage-orders-on-the-go';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note|null
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only add this note if this store is at least 6 months old.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'month-6+' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the previous mobile app notes have not been actioned.
|
||||
if ( MobileApp::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'Manage your orders on the go', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Look for orders, customer info, and process refunds in one click with the Woo app.', 'woocommerce' ) );
|
||||
$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(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Jetpack Marketing Note Provider.
|
||||
*
|
||||
* Adds notes to the merchant's inbox concerning Jetpack Backup.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
|
||||
/**
|
||||
* Suggest Jetpack Backup to Woo users.
|
||||
*
|
||||
* Note: This should probably live in the Jetpack plugin in the future.
|
||||
*
|
||||
* @see https://developer.woocommerce.com/2020/10/16/using-the-admin-notes-inbox-in-woocommerce/
|
||||
*/
|
||||
class MarketingJetpack {
|
||||
// Shared Note Traits.
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-marketing-jetpack-backup';
|
||||
|
||||
/**
|
||||
* Product IDs that include Backup.
|
||||
*/
|
||||
const BACKUP_IDS = [
|
||||
2010,
|
||||
2011,
|
||||
2012,
|
||||
2013,
|
||||
2014,
|
||||
2015,
|
||||
2100,
|
||||
2101,
|
||||
2102,
|
||||
2103,
|
||||
2005,
|
||||
2006,
|
||||
2000,
|
||||
2003,
|
||||
2001,
|
||||
2004,
|
||||
];
|
||||
|
||||
/**
|
||||
* Maybe add a note on Jetpack Backups for Jetpack sites older than a week without Backups.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
/**
|
||||
* Check if Jetpack is installed.
|
||||
*/
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
if ( ! in_array( 'jetpack', $installed_plugins, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data_store = \WC_Data_Store::load( 'admin-note' );
|
||||
|
||||
// Do we already have this note?
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
if ( ! empty( $note_ids ) ) {
|
||||
|
||||
$note_id = array_pop( $note_ids );
|
||||
$note = Notes::get_note( $note_id );
|
||||
if ( false === $note ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If Jetpack Backups was purchased after the note was created, mark this note as actioned.
|
||||
if ( self::has_backups() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) {
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
$note->save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check requirements.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', DAY_IN_SECONDS * 3 ) || ! self::can_be_added() || self::has_backups() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add note.
|
||||
$note = self::get_note();
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Protect your WooCommerce Store with Jetpack Backup.', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Store downtime means lost sales. One-click restores get you back online quickly if something goes wrong.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_layout( 'thumbnail' );
|
||||
$note->set_image(
|
||||
WC_ADMIN_IMAGES_FOLDER_URL . '/admin_notes/marketing-jetpack-2x.png'
|
||||
);
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin-notes' );
|
||||
$note->add_action(
|
||||
'jetpack-backup-woocommerce',
|
||||
__( 'Get backups', 'woocommerce' ),
|
||||
esc_url( 'https://jetpack.com/upgrade/backup-woocommerce/?utm_source=inbox&utm_medium=automattic_referred&utm_campaign=jp_backup_to_woo' ),
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this blog already has a Jetpack Backups product.
|
||||
*
|
||||
* @return boolean Whether or not this blog has backups.
|
||||
*/
|
||||
protected static function has_backups() {
|
||||
$product_ids = [];
|
||||
|
||||
$plan = get_option( 'jetpack_active_plan' );
|
||||
if ( ! empty( $plan ) ) {
|
||||
$product_ids[] = $plan['product_id'];
|
||||
}
|
||||
|
||||
$products = get_option( 'jetpack_site_products' );
|
||||
if ( ! empty( $products ) ) {
|
||||
foreach ( $products as $product ) {
|
||||
$product_ids[] = $product['product_id'];
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) array_intersect( self::BACKUP_IDS, $product_ids );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles merchant email notifications
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Merchant email notifications.
|
||||
* This gets all non-sent notes type `email` and sends them.
|
||||
*/
|
||||
class MerchantEmailNotifications {
|
||||
/**
|
||||
* Initialize the merchant email notifications.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'admin_init', array( __CLASS__, 'trigger_notification_action' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the note action.
|
||||
*/
|
||||
public static function trigger_notification_action() {
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
if (
|
||||
! isset( $_GET['external_redirect'] ) ||
|
||||
1 !== intval( $_GET['external_redirect'] ) ||
|
||||
! isset( $_GET['user'] ) ||
|
||||
! isset( $_GET['note'] ) ||
|
||||
! isset( $_GET['action'] )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$note_id = intval( $_GET['note'] );
|
||||
$action_id = intval( $_GET['action'] );
|
||||
$user_id = intval( $_GET['user'] );
|
||||
/* phpcs:enable */
|
||||
|
||||
$note = Notes::get_note( $note_id );
|
||||
|
||||
if ( ! $note || Note::E_WC_ADMIN_NOTE_EMAIL !== $note->get_type() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$triggered_action = Notes::get_action_by_id( $note, $action_id );
|
||||
|
||||
if ( ! $triggered_action ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notes::trigger_note_action( $note, $triggered_action );
|
||||
$url = $triggered_action->query;
|
||||
|
||||
// We will use "wp_safe_redirect" when it's an internal redirect.
|
||||
if ( strpos( $url, 'http' ) === false ) {
|
||||
wp_safe_redirect( $url );
|
||||
} else {
|
||||
header( 'Location: ' . $url );
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send all the notifications type `email`.
|
||||
*/
|
||||
public static function run() {
|
||||
$data_store = Notes::load_data_store();
|
||||
$notes = $data_store->get_notes(
|
||||
array(
|
||||
'type' => array( Note::E_WC_ADMIN_NOTE_EMAIL ),
|
||||
'status' => array( 'unactioned' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $notes as $note ) {
|
||||
$note = Notes::get_note( $note->note_id );
|
||||
if ( $note ) {
|
||||
self::send_merchant_notification( $note );
|
||||
$note->set_status( 'sent' );
|
||||
$note->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification to the merchant.
|
||||
*
|
||||
* @param object $note The note to send.
|
||||
*/
|
||||
public static function send_merchant_notification( $note ) {
|
||||
\WC_Emails::instance();
|
||||
$users = self::get_notification_recipients( $note );
|
||||
$email = new EmailNotification( $note );
|
||||
foreach ( $users as $user ) {
|
||||
if ( is_email( $user->user_email ) ) {
|
||||
$name = self::get_merchant_preferred_name( $user );
|
||||
$email->trigger( $user->user_email, $user->ID, $name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred name for user. First choice is
|
||||
* the user's first name, and then display_name.
|
||||
*
|
||||
* @param WP_User $user Recipient to send the note to.
|
||||
* @return string User's name.
|
||||
*/
|
||||
public static function get_merchant_preferred_name( $user ) {
|
||||
$first_name = get_user_meta( $user->ID, 'first_name', true );
|
||||
if ( $first_name ) {
|
||||
return $first_name;
|
||||
}
|
||||
if ( $user->display_name ) {
|
||||
return $user->display_name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users by role to notify.
|
||||
*
|
||||
* @param object $note The note to send.
|
||||
* @return array Users to notify
|
||||
*/
|
||||
public static function get_notification_recipients( $note ) {
|
||||
$content_data = $note->get_content_data();
|
||||
$role = 'administrator';
|
||||
if ( isset( $content_data->role ) ) {
|
||||
$role = $content_data->role;
|
||||
}
|
||||
$args = array( 'role' => $role );
|
||||
return get_users( $args );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Migrate from Shopify to WooCommerce.
|
||||
*
|
||||
* Adds a note to ask the client if they want to migrate from Shopify to WooCommerce.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Migrate_From_Shopify.
|
||||
*/
|
||||
class MigrateFromShopify {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-migrate-from-shopify';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
|
||||
// We want to show the note after two days.
|
||||
$two_days = 2 * DAY_IN_SECONDS;
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );
|
||||
if (
|
||||
! isset( $onboarding_profile['setup_client'] ) ||
|
||||
! isset( $onboarding_profile['selling_venues'] ) ||
|
||||
! isset( $onboarding_profile['other_platform'] )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the client is not setup.
|
||||
if ( $onboarding_profile['setup_client'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We will show the notification when the client already is selling and is using Shopify.
|
||||
if (
|
||||
'other' !== $onboarding_profile['selling_venues'] ||
|
||||
'shopify' !== $onboarding_profile['other_platform']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Do you want to migrate from Shopify to WooCommerce?', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Changing eCommerce platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'migrate-from-shopify',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/posts/migrate-from-shopify-to-woocommerce/?utm_source=inbox&utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Mobile App Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox showing the benefits of the mobile app.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Mobile_App
|
||||
*/
|
||||
class MobileApp {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-mobile-app';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// We want to show the mobile app note after day 2.
|
||||
$two_days_in_seconds = 2 * DAY_IN_SECONDS;
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days_in_seconds ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( 'Install the WooCommerce mobile app to manage orders, receive sales notifications, and view key metrics — wherever you are.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Install Woo mobile app', '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( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_medium=product' );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin (Dashboard) New Sales Record Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox when the previous day's sales are a new record.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* New_Sales_Record
|
||||
*/
|
||||
class NewSalesRecord {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-new-sales-record';
|
||||
|
||||
/**
|
||||
* Option name for the sales record date in ISO 8601 (YYYY-MM-DD) date.
|
||||
*/
|
||||
const RECORD_DATE_OPTION_KEY = 'woocommerce_sales_record_date';
|
||||
|
||||
/**
|
||||
* Option name for the sales record amount.
|
||||
*/
|
||||
const RECORD_AMOUNT_OPTION_KEY = 'woocommerce_sales_record_amount';
|
||||
|
||||
/**
|
||||
* Returns the total of yesterday's sales.
|
||||
*
|
||||
* @param string $date Date for sales to sum (i.e. YYYY-MM-DD).
|
||||
* @return floatval
|
||||
*/
|
||||
public static function sum_sales_for_date( $date ) {
|
||||
$order_query = new \WC_Order_Query( array( 'date_created' => $date ) );
|
||||
$orders = $order_query->get_orders();
|
||||
$total = 0;
|
||||
|
||||
foreach ( (array) $orders as $order ) {
|
||||
$total += $order->get_total();
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly add a sales record note.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
/**
|
||||
* Filter to allow for disabling sales record milestones.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param boolean default true
|
||||
*/
|
||||
$sales_record_notes_enabled = apply_filters( 'woocommerce_admin_sales_record_milestone_enabled', true );
|
||||
|
||||
if ( ! $sales_record_notes_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$yesterday = gmdate( 'Y-m-d', current_time( 'timestamp', 0 ) - DAY_IN_SECONDS );
|
||||
$total = self::sum_sales_for_date( $yesterday );
|
||||
|
||||
// No sales yesterday? Bail.
|
||||
if ( 0 >= $total ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record_date = get_option( self::RECORD_DATE_OPTION_KEY, '' );
|
||||
$record_amt = floatval( get_option( self::RECORD_AMOUNT_OPTION_KEY, 0 ) );
|
||||
|
||||
// No previous entry? Just enter what we have and return without generating a note.
|
||||
if ( empty( $record_date ) ) {
|
||||
update_option( self::RECORD_DATE_OPTION_KEY, $yesterday );
|
||||
update_option( self::RECORD_AMOUNT_OPTION_KEY, $total );
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, if yesterdays total bested the record, update AND generate a note.
|
||||
if ( $total > $record_amt ) {
|
||||
update_option( self::RECORD_DATE_OPTION_KEY, $yesterday );
|
||||
update_option( self::RECORD_AMOUNT_OPTION_KEY, $total );
|
||||
|
||||
// We only want one sales record note at any time in the inbox, so we delete any other first.
|
||||
Notes::delete_notes_with_name( self::NOTE_NAME );
|
||||
|
||||
$note = self::get_note_with_record_data( $record_date, $record_amt, $yesterday, $total );
|
||||
$note->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note with record data.
|
||||
*
|
||||
* @param string $record_date record date Y-m-d.
|
||||
* @param float $record_amt record amount.
|
||||
* @param string $yesterday yesterday's date Y-m-d.
|
||||
* @param string $total total sales for yesterday.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note_with_record_data( $record_date, $record_amt, $yesterday, $total ) {
|
||||
// Use F jS (March 7th) format for English speaking countries.
|
||||
if ( substr( get_user_locale(), 0, 2 ) === 'en' ) {
|
||||
$date_format = 'F jS';
|
||||
} else {
|
||||
// otherwise, fallback to the system date format.
|
||||
$date_format = get_option( 'date_format' );
|
||||
}
|
||||
|
||||
$formatted_yesterday = date_i18n( $date_format, strtotime( $yesterday ) );
|
||||
$formatted_total = html_entity_decode( wp_strip_all_tags( wc_price( $total ) ) );
|
||||
$formatted_record_date = date_i18n( $date_format, strtotime( $record_date ) );
|
||||
$formatted_record_amt = html_entity_decode( wp_strip_all_tags( wc_price( $record_amt ) ) );
|
||||
|
||||
$content = sprintf(
|
||||
/* translators: 1 and 4: Date (e.g. October 16th), 2 and 3: Amount (e.g. $160.00) */
|
||||
__( 'Woohoo, %1$s was your record day for sales! Net sales was %2$s beating the previous record of %3$s set on %4$s.', 'woocommerce' ),
|
||||
$formatted_yesterday,
|
||||
$formatted_total,
|
||||
$formatted_record_amt,
|
||||
$formatted_record_date
|
||||
);
|
||||
|
||||
$content_data = (object) array(
|
||||
'old_record_date' => $record_date,
|
||||
'old_record_amt' => $record_amt,
|
||||
'new_record_date' => $yesterday,
|
||||
'new_record_amt' => $total,
|
||||
);
|
||||
|
||||
$report_url = '?page=wc-admin&path=/analytics/revenue&period=custom&compare=previous_year&after=' . $yesterday . '&before=' . $yesterday;
|
||||
|
||||
// And now, create our new note.
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'New sales record!', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( $content_data );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action( 'view-report', __( 'View report', 'woocommerce' ), $report_url );
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note. This is used for localizing the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = Notes::get_note_by_name( self::NOTE_NAME );
|
||||
if ( ! $note ) {
|
||||
return false;
|
||||
}
|
||||
$content_data = $note->get_content_data();
|
||||
return self::get_note_with_record_data(
|
||||
$content_data->old_record_date,
|
||||
$content_data->old_record_amt,
|
||||
$content_data->new_record_date,
|
||||
$content_data->new_record_amt
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Payments reminder note.
|
||||
*
|
||||
* Adds a notes to complete the payment methods.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Onboarding_Payments.
|
||||
*/
|
||||
class OnboardingPayments {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-onboarding-payments-reminder';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// We want to show the note after five days.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 5 * DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if any gateways have been added.
|
||||
$gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||
$enabled_gateways = array_filter(
|
||||
$gateways,
|
||||
function( $gateway ) {
|
||||
return 'yes' === $gateway->enabled;
|
||||
}
|
||||
);
|
||||
if ( ! empty( $enabled_gateways ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Start accepting payments on your store!', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Take payments with the provider that’s right for you - choose from 100+ payment gateways for WooCommerce.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'view-payment-gateways',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED,
|
||||
true
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Start your online clothing store.
|
||||
*
|
||||
* Adds a note to ask the client if they are considering starting an online
|
||||
* clothing store.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Online_Clothing_Store.
|
||||
*/
|
||||
class OnlineClothingStore {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-online-clothing-store';
|
||||
|
||||
/**
|
||||
* Returns whether the industries includes fashion-apparel-accessories.
|
||||
*
|
||||
* @param array $industries The industries to search.
|
||||
*
|
||||
* @return bool Whether the industries includes fashion-apparel-accessories.
|
||||
*/
|
||||
private static function is_in_fashion_industry( $industries ) {
|
||||
foreach ( $industries as $industry ) {
|
||||
if ( 'fashion-apparel-accessories' === $industry['slug'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// We want to show the note after two days.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1', 2 * DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// We need to show the notification when the industry is
|
||||
// fashion/apparel/accessories.
|
||||
if ( ! isset( $onboarding_profile['industry'] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! self::is_in_fashion_industry( $onboarding_profile['industry'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Start your online clothing store', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Starting a fashion website is exciting but it may seem overwhelming as well. In this article, we\'ll walk you through the setup process, teach you to create successful product listings, and show you how to market to your ideal audience.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'online-clothing-store',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/posts/starting-an-online-clothing-store/?utm_source=inbox&utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin (Dashboard) Order Milestones Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox when certain order milestones are reached.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
/**
|
||||
* Order_Milestones
|
||||
*/
|
||||
class OrderMilestones {
|
||||
/**
|
||||
* Name of the "other milestones" note.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-orders-milestone';
|
||||
|
||||
/**
|
||||
* Option key name to store last order milestone.
|
||||
*/
|
||||
const LAST_ORDER_MILESTONE_OPTION_KEY = 'woocommerce_admin_last_orders_milestone';
|
||||
|
||||
/**
|
||||
* Hook to process order milestones.
|
||||
*/
|
||||
const PROCESS_ORDERS_MILESTONE_HOOK = 'wc_admin_process_orders_milestone';
|
||||
|
||||
/**
|
||||
* Allowed order statuses for calculating milestones.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $allowed_statuses = array(
|
||||
'pending',
|
||||
'processing',
|
||||
'completed',
|
||||
);
|
||||
|
||||
/**
|
||||
* Orders count cache.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $orders_count = null;
|
||||
|
||||
/**
|
||||
* Further order milestone thresholds.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $milestones = array(
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
250,
|
||||
500,
|
||||
1000,
|
||||
5000,
|
||||
10000,
|
||||
500000,
|
||||
1000000,
|
||||
);
|
||||
|
||||
/**
|
||||
* Delay hook attachment until after the WC post types have been registered.
|
||||
*
|
||||
* This is required for retrieving the order count.
|
||||
*/
|
||||
public function __construct() {
|
||||
/**
|
||||
* Filter Order statuses that will count towards milestones.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param array $allowed_statuses Order statuses that will count towards milestones.
|
||||
*/
|
||||
$this->allowed_statuses = apply_filters( 'woocommerce_admin_order_milestone_statuses', $this->allowed_statuses );
|
||||
|
||||
add_action( 'woocommerce_after_register_post_type', array( $this, 'init' ) );
|
||||
register_deactivation_hook( WC_PLUGIN_FILE, array( $this, 'clear_scheduled_event' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook everything up.
|
||||
*/
|
||||
public function init() {
|
||||
if ( ! wp_next_scheduled( self::PROCESS_ORDERS_MILESTONE_HOOK ) ) {
|
||||
wp_schedule_event( time(), 'hourly', self::PROCESS_ORDERS_MILESTONE_HOOK );
|
||||
}
|
||||
|
||||
add_action( 'wc_admin_installed', array( $this, 'backfill_last_milestone' ) );
|
||||
|
||||
add_action( self::PROCESS_ORDERS_MILESTONE_HOOK, array( $this, 'possibly_add_note' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear out our hourly milestone hook upon plugin deactivation.
|
||||
*/
|
||||
public function clear_scheduled_event() {
|
||||
wp_clear_scheduled_hook( self::PROCESS_ORDERS_MILESTONE_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count of orders (in the allowed statuses).
|
||||
*
|
||||
* @param bool $no_cache Optional. Skip cache.
|
||||
* @return int Total orders count.
|
||||
*/
|
||||
public function get_orders_count( $no_cache = false ) {
|
||||
if ( $no_cache || is_null( $this->orders_count ) ) {
|
||||
$status_counts = array_map( 'wc_orders_count', $this->allowed_statuses );
|
||||
$this->orders_count = array_sum( $status_counts );
|
||||
}
|
||||
|
||||
return $this->orders_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill the store's current milestone.
|
||||
*
|
||||
* Used to avoid celebrating milestones that were reached before plugin activation.
|
||||
*/
|
||||
public function backfill_last_milestone() {
|
||||
// If the milestone notes have been disabled via filter, bail.
|
||||
if ( ! $this->are_milestones_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set_last_milestone( $this->get_current_milestone() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store's last milestone.
|
||||
*
|
||||
* @return int Last milestone reached.
|
||||
*/
|
||||
public function get_last_milestone() {
|
||||
return get_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last reached milestone.
|
||||
*
|
||||
* @param int $milestone Last milestone reached.
|
||||
*/
|
||||
public function set_last_milestone( $milestone ) {
|
||||
update_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, $milestone );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the current orders milestone.
|
||||
*
|
||||
* Based on the threshold values in $this->milestones.
|
||||
*
|
||||
* @return int Current orders milestone.
|
||||
*/
|
||||
public function get_current_milestone() {
|
||||
$milestone_reached = 0;
|
||||
$orders_count = $this->get_orders_count();
|
||||
|
||||
foreach ( $this->milestones as $milestone ) {
|
||||
if ( $milestone <= $orders_count ) {
|
||||
$milestone_reached = $milestone;
|
||||
}
|
||||
}
|
||||
|
||||
return $milestone_reached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate note title for a given milestone.
|
||||
*
|
||||
* @param int $milestone Order milestone.
|
||||
* @return string Note title for the milestone.
|
||||
*/
|
||||
public static function get_note_title_for_milestone( $milestone ) {
|
||||
switch ( $milestone ) {
|
||||
case 1:
|
||||
return __( 'First order received', 'woocommerce' );
|
||||
case 10:
|
||||
case 100:
|
||||
case 250:
|
||||
case 500:
|
||||
case 1000:
|
||||
case 5000:
|
||||
case 10000:
|
||||
case 500000:
|
||||
case 1000000:
|
||||
return sprintf(
|
||||
/* translators: Number of orders processed. */
|
||||
__( 'Congratulations on processing %s orders!', 'woocommerce' ),
|
||||
wc_format_decimal( $milestone )
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate note content for a given milestone.
|
||||
*
|
||||
* @param int $milestone Order milestone.
|
||||
* @return string Note content for the milestone.
|
||||
*/
|
||||
public static function get_note_content_for_milestone( $milestone ) {
|
||||
switch ( $milestone ) {
|
||||
case 1:
|
||||
return __( 'Congratulations on getting your first order! Now is a great time to learn how to manage your orders.', 'woocommerce' );
|
||||
case 10:
|
||||
return __( "You've hit the 10 orders milestone! Look at you go. Browse some WooCommerce success stories for inspiration.", 'woocommerce' );
|
||||
case 100:
|
||||
case 250:
|
||||
case 500:
|
||||
case 1000:
|
||||
case 5000:
|
||||
case 10000:
|
||||
case 500000:
|
||||
case 1000000:
|
||||
return __( 'Another order milestone! Take a look at your Orders Report to review your orders to date.', 'woocommerce' );
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate note action for a given milestone.
|
||||
*
|
||||
* @param int $milestone Order milestone.
|
||||
* @return array Note actoion (name, label, query) for the milestone.
|
||||
*/
|
||||
public static function get_note_action_for_milestone( $milestone ) {
|
||||
switch ( $milestone ) {
|
||||
case 1:
|
||||
return array(
|
||||
'name' => 'learn-more',
|
||||
'label' => __( 'Learn more', 'woocommerce' ),
|
||||
'query' => 'https://woocommerce.com/document/managing-orders/?utm_source=inbox&utm_medium=product',
|
||||
);
|
||||
case 10:
|
||||
return array(
|
||||
'name' => 'browse',
|
||||
'label' => __( 'Browse', 'woocommerce' ),
|
||||
'query' => 'https://woocommerce.com/success-stories/?utm_source=inbox&utm_medium=product',
|
||||
);
|
||||
case 100:
|
||||
case 250:
|
||||
case 500:
|
||||
case 1000:
|
||||
case 5000:
|
||||
case 10000:
|
||||
case 500000:
|
||||
case 1000000:
|
||||
return array(
|
||||
'name' => 'review-orders',
|
||||
'label' => __( 'Review your orders', 'woocommerce' ),
|
||||
'query' => '?page=wc-admin&path=/analytics/orders',
|
||||
);
|
||||
default:
|
||||
return array(
|
||||
'name' => '',
|
||||
'label' => '',
|
||||
'query' => '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to see if the milestone notes are enabled.
|
||||
*
|
||||
* @return boolean True if milestone notifications are enabled.
|
||||
*/
|
||||
public function are_milestones_enabled() {
|
||||
/**
|
||||
* Filter to allow for disabling order milestones.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param boolean default true
|
||||
*/
|
||||
$milestone_notes_enabled = apply_filters( 'woocommerce_admin_order_milestones_enabled', true );
|
||||
|
||||
return $milestone_notes_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note. This is used for localizing the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = Notes::get_note_by_name( self::NOTE_NAME );
|
||||
if ( ! $note ) {
|
||||
return false;
|
||||
}
|
||||
$content_data = $note->get_content_data();
|
||||
if ( ! isset( $content_data->current_milestone ) ) {
|
||||
return false;
|
||||
}
|
||||
return self::get_note_by_milestone(
|
||||
$content_data->current_milestone
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note by milestones.
|
||||
*
|
||||
* @param int $current_milestone Current milestone.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note_by_milestone( $current_milestone ) {
|
||||
$content_data = (object) array(
|
||||
'current_milestone' => $current_milestone,
|
||||
);
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( self::get_note_title_for_milestone( $current_milestone ) );
|
||||
$note->set_content( self::get_note_content_for_milestone( $current_milestone ) );
|
||||
$note->set_content_data( $content_data );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note_action = self::get_note_action_for_milestone( $current_milestone );
|
||||
$note->add_action( $note_action['name'], $note_action['label'], $note_action['query'] );
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a note can and should be added.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_be_added() {
|
||||
// If the milestone notes have been disabled via filter, bail.
|
||||
if ( ! $this->are_milestones_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last_milestone = $this->get_last_milestone();
|
||||
$current_milestone = $this->get_current_milestone();
|
||||
|
||||
if ( $current_milestone <= $last_milestone ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add milestone notes for other significant thresholds.
|
||||
*/
|
||||
public function possibly_add_note() {
|
||||
if ( ! self::can_be_added() ) {
|
||||
return;
|
||||
}
|
||||
$current_milestone = $this->get_current_milestone();
|
||||
$this->set_last_milestone( $current_milestone );
|
||||
|
||||
// We only want one milestone note at any time.
|
||||
Notes::delete_notes_with_name( self::NOTE_NAME );
|
||||
$note = $this->get_note_by_milestone( $current_milestone );
|
||||
$note->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Payments More Info Needed Inbox Note Provider
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* PaymentsMoreInfoNeeded
|
||||
*/
|
||||
class PaymentsMoreInfoNeeded {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-payments-more-info-needed';
|
||||
|
||||
/**
|
||||
* Should this note exist?
|
||||
*/
|
||||
public static function is_applicable() {
|
||||
return self::should_display_note();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should display the note.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// 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( 'wcpay_welcome_page_exit_survey_more_info_needed_timestamp', false );
|
||||
if ( ! $exit_survey_timestamp ||
|
||||
( time() - $exit_survey_timestamp < 30 * DAY_IN_SECONDS )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$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 WooPayments', '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( 'learn-more', __( 'Learn more here', 'woocommerce' ), 'https://woocommerce.com/payments/' );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Payment Reminder Me later
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* PaymentsRemindMeLater
|
||||
*/
|
||||
class PaymentsRemindMeLater {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-payments-remind-me-later';
|
||||
|
||||
/**
|
||||
* Should this note exist?
|
||||
*/
|
||||
public static function is_applicable() {
|
||||
return self::should_display_note();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should display the note.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// 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( 'wcpay_welcome_page_viewed_timestamp', false );
|
||||
if ( ! $view_timestamp ||
|
||||
( time() - $view_timestamp < 3 * DAY_IN_SECONDS )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$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 WooPayments', '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( 'learn-more', __( 'Learn more', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' ) );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Performance on mobile note.
|
||||
*
|
||||
* Adds a note to download the mobile app, performance on mobile.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Performance_On_Mobile
|
||||
*/
|
||||
class PerformanceOnMobile {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-performance-on-mobile';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only add this note if this store is at least 9 months old.
|
||||
$nine_months_in_seconds = MONTH_IN_SECONDS * 9;
|
||||
if ( ! self::wc_admin_active_for( $nine_months_in_seconds ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the previous mobile app notes have not been actioned.
|
||||
if ( MobileApp::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( RealTimeOrderAlerts::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
if ( ManageOrdersOnTheGo::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'Track your store performance on mobile', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Monitor your sales and high performing products with the Woo app.', 'woocommerce' ) );
|
||||
$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(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Personalize Your Store Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox prompting them to personalize their store.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Personalize_Store
|
||||
*/
|
||||
class PersonalizeStore {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-personalize-store';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only show the note to stores with homepage.
|
||||
$homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false );
|
||||
if ( ! $homepage_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the note after task list is done.
|
||||
$is_task_list_complete = get_option( 'woocommerce_task_list_complete', false );
|
||||
|
||||
// We want to show the note after day 5.
|
||||
$five_days_in_seconds = 5 * DAY_IN_SECONDS;
|
||||
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', $five_days_in_seconds ) && ! $is_task_list_complete ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( 'The homepage is one of the most important entry points in your store. When done right it can lead to higher conversions and engagement. Don\'t forget to personalize the homepage that we created for your store during the onboarding.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Personalize your store\'s homepage', '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( 'personalize-homepage', __( 'Personalize homepage', 'woocommerce' ), admin_url( 'post.php?post=' . $homepage_id . '&action=edit' ), Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Real Time Order Alerts Note.
|
||||
*
|
||||
* Adds a note to download the mobile app to monitor store activity.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Real_Time_Order_Alerts
|
||||
*/
|
||||
class RealTimeOrderAlerts {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-real-time-order-alerts';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only add this note if the store is 3 months old.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'month-3-6' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the previous mobile app note was not actioned.
|
||||
if ( MobileApp::has_note_been_actioned() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( 'Get notifications about store activity, including new orders and product reviews directly on your mobile devices with the Woo app.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Get real-time order alerts anywhere', '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( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product' );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: Selling Online Courses note
|
||||
*
|
||||
* Adds a note to encourage selling online courses.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
|
||||
/**
|
||||
* Selling_Online_Courses
|
||||
*/
|
||||
class SellingOnlineCourses {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-selling-online-courses';
|
||||
|
||||
/**
|
||||
* Attach hooks.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action(
|
||||
'update_option_' . OnboardingProfile::DATA_OPTION,
|
||||
array( $this, 'check_onboarding_profile' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the profiler options match before possibly adding note.
|
||||
*
|
||||
* @param object $old_value The old option value.
|
||||
* @param object $value The new option value.
|
||||
* @param string $option The name of the option.
|
||||
*/
|
||||
public static function check_onboarding_profile( $old_value, $value, $option ) {
|
||||
// Skip adding if this store is in the education/learning industry.
|
||||
if ( ! isset( $value['industry'] ) ) {
|
||||
return;
|
||||
}
|
||||
$industry_slugs = array_column( $value['industry'], 'slug' );
|
||||
if ( ! in_array( 'education-and-learning', $industry_slugs, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::possibly_add_note();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'Do you want to sell online courses?', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Online courses are a great solution for any business that can teach a new skill. Since courses don’t require physical product development or shipping, they’re affordable, fast to create, and can generate passive income for years to come. In this article, we provide you more information about selling courses using WooCommerce.', 'woocommerce' ) );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://woocommerce.com/posts/how-to-sell-online-courses-wordpress/?utm_source=inbox&utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Usage Tracking Opt In Note Provider.
|
||||
*
|
||||
* Adds a Usage Tracking Opt In extension note.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Tracking_Opt_In
|
||||
*/
|
||||
class TrackingOptIn {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-usage-tracking-opt-in';
|
||||
|
||||
/**
|
||||
* Attach hooks.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_note_action_tracking-opt-in', array( $this, 'opt_in_to_tracking' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note|null
|
||||
*/
|
||||
public static function get_note() {
|
||||
// Only show this note to stores that are opted out.
|
||||
if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to show the note after one week.
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* translators: 1: open link to WooCommerce.com settings, 2: open link to WooCommerce.com tracking documentation, 3: close link tag. */
|
||||
$content_format = __(
|
||||
'Gathering usage data allows us to improve WooCommerce. Your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense. You can always visit the %1$sSettings%3$s and choose to stop sharing data. %2$sRead more%3$s about what data we collect.',
|
||||
'woocommerce'
|
||||
);
|
||||
|
||||
$note_content = sprintf(
|
||||
$content_format,
|
||||
'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ) ) . '" target="_blank">',
|
||||
'<a href="https://woocommerce.com/usage-tracking?utm_medium=product" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Help WooCommerce improve with usage tracking', 'woocommerce' ) );
|
||||
$note->set_content( $note_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( 'tracking-opt-in', __( 'Activate usage tracking', 'woocommerce' ), false, Note::E_WC_ADMIN_NOTE_ACTIONED, true );
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt in to usage tracking when note is actioned.
|
||||
*
|
||||
* @param Note $note Note being acted upon.
|
||||
*/
|
||||
public function opt_in_to_tracking( $note ) {
|
||||
if ( self::NOTE_NAME === $note->get_name() ) {
|
||||
// Opt in to tracking and schedule the first data update.
|
||||
// Same mechanism as in WC_Admin_Setup_Wizard::wc_setup_store_setup_save().
|
||||
update_option( 'woocommerce_allow_tracking', 'yes' );
|
||||
wp_schedule_single_event( time() + 10, 'woocommerce_tracker_send_event', array( true ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Unsecured Files Note.
|
||||
*
|
||||
* Adds a warning about potentially unsecured files.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
|
||||
if ( ! class_exists( Note::class ) ) {
|
||||
class_alias( WC_Admin_Note::class, Note::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsecured_Report_Files
|
||||
*/
|
||||
class UnsecuredReportFiles {
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-remove-unsecured-report-files';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note|null
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Potentially unsecured files were found in your uploads directory', 'woocommerce' ) );
|
||||
$note->set_content(
|
||||
sprintf(
|
||||
/* translators: 1: opening analytics docs link tag. 2: closing link tag */
|
||||
__( 'Files that may contain %1$sstore analytics%2$s reports were found in your uploads directory - we recommend assessing and deleting any such files.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/woocommerce-analytics/" target="_blank">',
|
||||
'</a>'
|
||||
)
|
||||
);
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_ERROR );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'learn-more',
|
||||
__( 'Learn more', 'woocommerce' ),
|
||||
'https://developer.woocommerce.com/2021/09/22/important-security-patch-released-in-woocommerce/',
|
||||
Note::E_WC_ADMIN_NOTE_UNACTIONED,
|
||||
true
|
||||
);
|
||||
$note->add_action(
|
||||
'dismiss',
|
||||
__( 'Dismiss', 'woocommerce' ),
|
||||
wc_admin_url(),
|
||||
Note::E_WC_ADMIN_NOTE_ACTIONED,
|
||||
false
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the note if it passes predefined conditions.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
$note = self::get_note();
|
||||
|
||||
if ( self::note_exists() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the note has been previously added.
|
||||
*/
|
||||
public static function note_exists() {
|
||||
$data_store = \WC_Data_Store::load( 'admin-note' );
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
return ! empty( $note_ids );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin WooCommerce Payments Note Provider.
|
||||
*
|
||||
* Adds a note to the merchant's inbox showing the benefits of the WooCommerce Payments.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* WooCommerce_Payments
|
||||
*/
|
||||
class WooCommercePayments {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-woocommerce-payments';
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const PLUGIN_SLUG = 'woocommerce-payments';
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const PLUGIN_FILE = 'woocommerce-payments/woocommerce-payments.php';
|
||||
|
||||
/**
|
||||
* Attach hooks.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'install_on_action' ) );
|
||||
add_action( 'wc-admin-woocommerce-payments_add_note', array( $this, 'add_note' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add a note on WooCommerce Payments for US based sites older than a week without the plugin installed.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) || 'US' !== WC()->countries->get_base_country() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data_store = Notes::load_data_store();
|
||||
|
||||
// We already have this note? Then mark the note as actioned.
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
if ( ! empty( $note_ids ) ) {
|
||||
|
||||
$note_id = array_pop( $note_ids );
|
||||
$note = Notes::get_note( $note_id );
|
||||
if ( false === $note ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the WooCommerce Payments plugin was installed after the note was created, make sure it's marked as actioned.
|
||||
if ( self::is_installed() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) {
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
$note->save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$current_date = new \DateTime();
|
||||
$publish_date = new \DateTime( '2020-04-14' );
|
||||
|
||||
if ( $current_date >= $publish_date ) {
|
||||
|
||||
$note = self::get_note();
|
||||
if ( self::can_be_added() ) {
|
||||
$note->save();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} else {
|
||||
|
||||
$hook_name = sprintf( '%s_add_note', self::NOTE_NAME );
|
||||
|
||||
if ( ! WC()->queue()->get_next( $hook_name ) ) {
|
||||
WC()->queue()->schedule_single( $publish_date->getTimestamp(), $hook_name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note about WooCommerce Payments.
|
||||
*
|
||||
* @return Note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$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>WooPayments</strong>.', 'woocommerce' ) .
|
||||
'<br><br>' .
|
||||
sprintf(
|
||||
/* translators: 1: opening link tag, 2: closing tag */
|
||||
__( 'By clicking "Get started", you agree to our %1$sTerms of Service%2$s', 'woocommerce' ),
|
||||
'<a href="https://wordpress.com/tos/" target="_blank">',
|
||||
'</a>'
|
||||
)
|
||||
);
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/payments/?utm_medium=product', Note::E_WC_ADMIN_NOTE_UNACTIONED );
|
||||
$note->add_action( 'get-started', __( 'Get started', 'woocommerce' ), wc_admin_url( '&action=setup-woocommerce-payments' ), Note::E_WC_ADMIN_NOTE_ACTIONED, true );
|
||||
$note->add_nonce_to_action( 'get-started', 'setup-woocommerce-payments', '' );
|
||||
|
||||
// Create the note as "actioned" if the plugin is already installed.
|
||||
if ( self::is_installed() ) {
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
}
|
||||
return $note;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the WooCommerce Payments plugin is active or installed.
|
||||
*/
|
||||
protected static function is_installed() {
|
||||
if ( defined( 'WC_Payments' ) ) {
|
||||
return true;
|
||||
}
|
||||
include_once ABSPATH . '/wp-admin/includes/plugin.php';
|
||||
return 0 === validate_plugin( self::PLUGIN_FILE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Install and activate WooCommerce Payments.
|
||||
*
|
||||
* @return boolean Whether the plugin was successfully activated.
|
||||
*/
|
||||
private function install_and_activate_wcpay() {
|
||||
$install_request = array( 'plugins' => self::PLUGIN_SLUG );
|
||||
$installer = new \Automattic\WooCommerce\Admin\API\Plugins();
|
||||
$result = $installer->install_plugins( $install_request );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wc_admin_record_tracks_event( 'woocommerce_payments_install', array( 'context' => 'inbox' ) );
|
||||
|
||||
$activate_request = array( 'plugins' => self::PLUGIN_SLUG );
|
||||
$result = $installer->activate_plugins( $activate_request );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install & activate WooCommerce Payments plugin, and redirect to setup.
|
||||
*/
|
||||
public function install_on_action() {
|
||||
// TODO: Need to validate this request more strictly since we're taking install actions directly?
|
||||
if (
|
||||
! isset( $_GET['page'] ) ||
|
||||
'wc-admin' !== $_GET['page'] ||
|
||||
! isset( $_GET['action'] ) ||
|
||||
'setup-woocommerce-payments' !== $_GET['action']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data_store = Notes::load_data_store();
|
||||
|
||||
// We already have this note? Then mark the note as actioned.
|
||||
$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
|
||||
if ( empty( $note_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note_id = array_pop( $note_ids );
|
||||
$note = Notes::get_note( $note_id );
|
||||
if ( false === $note ) {
|
||||
return;
|
||||
}
|
||||
$action = $note->get_action( 'get-started' );
|
||||
if ( ! $action ||
|
||||
( isset( $action->nonce_action ) &&
|
||||
(
|
||||
empty( $_GET['_wpnonce'] ) ||
|
||||
! wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), $action->nonce_action ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'install_plugins' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->install_and_activate_wcpay();
|
||||
|
||||
// WooCommerce Payments is installed at this point, so link straight into the onboarding flow.
|
||||
$connect_url = add_query_arg(
|
||||
array(
|
||||
'wcpay-connect' => '1',
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay-connect' ),
|
||||
),
|
||||
admin_url()
|
||||
);
|
||||
wp_safe_redirect( $connect_url );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin: WooCommerce Subscriptions.
|
||||
*
|
||||
* Adds a note to learn more about WooCommerce Subscriptions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
|
||||
/**
|
||||
* WooCommerce_Subscriptions.
|
||||
*/
|
||||
class WooCommerceSubscriptions {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-woocommerce-subscriptions';
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note|null
|
||||
*/
|
||||
public static function get_note() {
|
||||
$onboarding_data = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
|
||||
if ( ! isset( $onboarding_data['product_types'] ) || ! in_array( 'subscriptions', $onboarding_data['product_types'], true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Do you need more info about WooCommerce Subscriptions?', 'woocommerce' ) );
|
||||
$note->set_content( __( 'WooCommerce Subscriptions allows you to introduce a variety of subscriptions for physical or virtual products and services. Create product-of-the-month clubs, weekly service subscriptions or even yearly software billing packages. Add sign-up fees, offer free trials, or set expiration periods.', 'woocommerce' ) );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'learn-more',
|
||||
__( 'Learn More', 'woocommerce' ),
|
||||
'https://woocommerce.com/products/woocommerce-subscriptions/?utm_source=inbox&utm_medium=product',
|
||||
Note::E_WC_ADMIN_NOTE_UNACTIONED,
|
||||
true
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin (Dashboard) WooCommerce.com Extension Subscriptions Note Provider.
|
||||
*
|
||||
* Adds notes to the merchant's inbox concerning WooCommerce.com extension subscriptions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Woo_Subscriptions_Notes
|
||||
*/
|
||||
class WooSubscriptionsNotes {
|
||||
const LAST_REFRESH_OPTION_KEY = 'woocommerce_admin-wc-helper-last-refresh';
|
||||
const NOTE_NAME = 'wc-admin-wc-helper-connection';
|
||||
const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection';
|
||||
const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription';
|
||||
const NOTIFY_WHEN_DAYS_LEFT = 60;
|
||||
|
||||
/**
|
||||
* We want to bubble up expiration notices when they cross certain age
|
||||
* thresholds. PHP 5.2 doesn't support constant arrays, so we do this.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_bump_thresholds() {
|
||||
return array( 60, 45, 20, 7, 1 ); // days.
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook all the things.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_head', array( $this, 'admin_head' ) );
|
||||
add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to changes in the helper option.
|
||||
*
|
||||
* @param array $old_value The previous value of the option.
|
||||
* @param array $value The new value of the option.
|
||||
*/
|
||||
public function update_option_woocommerce_helper_data( $old_value, $value ) {
|
||||
if ( ! is_array( $old_value ) ) {
|
||||
$old_value = array();
|
||||
}
|
||||
if ( ! is_array( $value ) ) {
|
||||
$value = array();
|
||||
}
|
||||
|
||||
$old_auth = array_key_exists( 'auth', $old_value ) ? $old_value['auth'] : array();
|
||||
$new_auth = array_key_exists( 'auth', $value ) ? $value['auth'] : array();
|
||||
$old_token = array_key_exists( 'access_token', $old_auth ) ? $old_auth['access_token'] : '';
|
||||
$new_token = array_key_exists( 'access_token', $new_auth ) ? $new_auth['access_token'] : '';
|
||||
|
||||
// The site just disconnected.
|
||||
if ( ! empty( $old_token ) && empty( $new_token ) ) {
|
||||
$this->remove_notes();
|
||||
$this->add_no_connection_note();
|
||||
return;
|
||||
}
|
||||
|
||||
// The site is connected.
|
||||
if ( $this->is_connected() ) {
|
||||
$this->remove_notes();
|
||||
$this->refresh_subscription_notes();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on `admin_head` hook. Checks the connection and refreshes subscription notes on relevant pages.
|
||||
*/
|
||||
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() ) {
|
||||
$refresh_notes = false;
|
||||
|
||||
// Did the user just do something on the helper page?.
|
||||
if ( isset( $_GET['wc-helper-status'] ) ) { // @codingStandardsIgnoreLine.
|
||||
$refresh_notes = true;
|
||||
}
|
||||
|
||||
// Has it been more than a day since we last checked?
|
||||
// Note: We do it this way and not wp_scheduled_task since WC_Helper_Options is not loaded for cron.
|
||||
$time_now_gmt = current_time( 'timestamp', 0 );
|
||||
$last_refresh = intval( get_option( self::LAST_REFRESH_OPTION_KEY, 0 ) );
|
||||
if ( $last_refresh + DAY_IN_SECONDS <= $time_now_gmt ) {
|
||||
update_option( self::LAST_REFRESH_OPTION_KEY, $time_now_gmt );
|
||||
$refresh_notes = true;
|
||||
}
|
||||
|
||||
if ( $refresh_notes ) {
|
||||
$this->refresh_subscription_notes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the connection. Adds a note (as necessary) if there is no connection.
|
||||
*/
|
||||
public function check_connection() {
|
||||
if ( ! $this->is_connected() ) {
|
||||
$data_store = Notes::load_data_store();
|
||||
$note_ids = $data_store->get_notes_with_name( self::CONNECTION_NOTE_NAME );
|
||||
if ( ! empty( $note_ids ) ) {
|
||||
// We already have a connection note. Exit early.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->remove_notes();
|
||||
$this->add_no_connection_note();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we think the site is currently connected to WooCommerce.com.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_connected() {
|
||||
$auth = \WC_Helper_Options::get( 'auth' );
|
||||
return ( ! empty( $auth['access_token'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WooCommerce.com provided site ID for this site.
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function get_connected_site_id() {
|
||||
if ( ! $this->is_connected() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth = \WC_Helper_Options::get( 'auth' );
|
||||
return absint( $auth['site_id'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of product_ids whose subscriptions are active on this site.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_subscription_active_product_ids() {
|
||||
$site_id = $this->get_connected_site_id();
|
||||
if ( ! $site_id ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$product_ids = array();
|
||||
|
||||
if ( $this->is_connected() ) {
|
||||
$subscriptions = \WC_Helper::get_subscriptions();
|
||||
|
||||
foreach ( (array) $subscriptions as $subscription ) {
|
||||
if ( in_array( $site_id, $subscription['connections'], true ) ) {
|
||||
$product_ids[] = $subscription['product_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $product_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all connection or subscription notes.
|
||||
*/
|
||||
public function remove_notes() {
|
||||
Notes::delete_notes_with_name( self::CONNECTION_NOTE_NAME );
|
||||
Notes::delete_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a note prompting to connect to WooCommerce.com.
|
||||
*/
|
||||
public function add_no_connection_note() {
|
||||
$note = self::get_note();
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WooCommerce.com connection note
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Connect to WooCommerce.com', 'woocommerce' ) );
|
||||
$note->set_content( __( 'Connect to get important product notifications and updates.', 'woocommerce' ) );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::CONNECTION_NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action(
|
||||
'connect',
|
||||
__( 'Connect', 'woocommerce' ),
|
||||
'?page=wc-addons§ion=helper',
|
||||
Note::E_WC_ADMIN_NOTE_UNACTIONED
|
||||
);
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product_id (if any) associated with a note.
|
||||
*
|
||||
* @param Note $note The note object to interrogate.
|
||||
* @return int|false
|
||||
*/
|
||||
public function get_product_id_from_subscription_note( &$note ) {
|
||||
$content_data = $note->get_content_data();
|
||||
|
||||
if ( property_exists( $content_data, 'product_id' ) ) {
|
||||
return intval( $content_data->product_id );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes notes for product_ids no longer active on this site.
|
||||
*/
|
||||
public function prune_inactive_subscription_notes() {
|
||||
$active_product_ids = $this->get_subscription_active_product_ids();
|
||||
|
||||
$data_store = Notes::load_data_store();
|
||||
$note_ids = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
|
||||
|
||||
foreach ( (array) $note_ids as $note_id ) {
|
||||
$note = Notes::get_note( $note_id );
|
||||
$product_id = $this->get_product_id_from_subscription_note( $note );
|
||||
if ( ! empty( $product_id ) ) {
|
||||
if ( ! in_array( $product_id, $active_product_ids, true ) ) {
|
||||
$note->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a note for a given product ID, if the note exists at all.
|
||||
*
|
||||
* @param int $product_id The product ID to search for.
|
||||
* @return Note|false
|
||||
*/
|
||||
public function find_note_for_product_id( $product_id ) {
|
||||
$product_id = intval( $product_id );
|
||||
|
||||
$data_store = Notes::load_data_store();
|
||||
$note_ids = $data_store->get_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
|
||||
foreach ( (array) $note_ids as $note_id ) {
|
||||
$note = Notes::get_note( $note_id );
|
||||
$found_product_id = $this->get_product_id_from_subscription_note( $note );
|
||||
|
||||
if ( $product_id === $found_product_id ) {
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a note for a given product ID, if the note exists at all.
|
||||
*
|
||||
* @param int $product_id The product ID to search for.
|
||||
*/
|
||||
public function delete_any_note_for_product_id( $product_id ) {
|
||||
$product_id = intval( $product_id );
|
||||
|
||||
$note = $this->find_note_for_product_id( $product_id );
|
||||
if ( $note ) {
|
||||
$note->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates a note for an expiring subscription.
|
||||
*
|
||||
* @param array $subscription The subscription to work with.
|
||||
*/
|
||||
public function add_or_update_subscription_expiring( $subscription ) {
|
||||
$product_id = $subscription['product_id'];
|
||||
$product_name = $subscription['product_name'];
|
||||
$expires = intval( $subscription['expires'] );
|
||||
$time_now_gmt = current_time( 'timestamp', 0 );
|
||||
$days_until_expiration = intval( ceil( ( $expires - $time_now_gmt ) / DAY_IN_SECONDS ) );
|
||||
|
||||
$note = $this->find_note_for_product_id( $product_id );
|
||||
|
||||
if ( $note ) {
|
||||
$content_data = $note->get_content_data();
|
||||
if ( property_exists( $content_data, 'days_until_expiration' ) ) {
|
||||
// Note: There is no reason this property should not exist. This is just defensive programming.
|
||||
$note_days_until_expiration = intval( $content_data->days_until_expiration );
|
||||
if ( $days_until_expiration === $note_days_until_expiration ) {
|
||||
// Note is already up to date. Bail.
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a note and we are at or have crossed a threshold, we should delete
|
||||
// the old note and create a new one, thereby "bumping" the note to the top of the inbox.
|
||||
$bump_thresholds = $this->get_bump_thresholds();
|
||||
$crossing_threshold = false;
|
||||
|
||||
foreach ( (array) $bump_thresholds as $bump_threshold ) {
|
||||
if ( ( $note_days_until_expiration > $bump_threshold ) && ( $days_until_expiration <= $bump_threshold ) ) {
|
||||
$note->delete();
|
||||
$note = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$note_title = sprintf(
|
||||
/* translators: name of the extension subscription expiring soon */
|
||||
__( '%s subscription expiring soon', 'woocommerce' ),
|
||||
$product_name
|
||||
);
|
||||
|
||||
$note_content = sprintf(
|
||||
/* translators: number of days until the subscription expires */
|
||||
__( 'Your subscription expires in %d days. Enable autorenew to avoid losing updates and access to support.', 'woocommerce' ),
|
||||
$days_until_expiration
|
||||
);
|
||||
|
||||
$note_content_data = (object) array(
|
||||
'product_id' => $product_id,
|
||||
'product_name' => $product_name,
|
||||
'expired' => false,
|
||||
'days_until_expiration' => $days_until_expiration,
|
||||
);
|
||||
|
||||
if ( ! $note ) {
|
||||
$note = new Note();
|
||||
}
|
||||
|
||||
// Reset everything in case we are repurposing an expired note as an expiring note.
|
||||
$note->set_title( $note_title );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_WARNING );
|
||||
$note->set_name( self::SUBSCRIPTION_NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->clear_actions();
|
||||
$note->add_action(
|
||||
'enable-autorenew',
|
||||
__( 'Enable Autorenew', 'woocommerce' ),
|
||||
'https://woocommerce.com/my-account/my-subscriptions/?utm_medium=product'
|
||||
);
|
||||
$note->set_content( $note_content );
|
||||
$note->set_content_data( $note_content_data );
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a note for an expired subscription, or updates an expiring note to expired.
|
||||
*
|
||||
* @param array $subscription The subscription to work with.
|
||||
*/
|
||||
public function add_or_update_subscription_expired( $subscription ) {
|
||||
$product_id = $subscription['product_id'];
|
||||
$product_name = $subscription['product_name'];
|
||||
$product_page = $subscription['product_url'];
|
||||
$expires = intval( $subscription['expires'] );
|
||||
$expires_date = gmdate( 'F jS', $expires );
|
||||
|
||||
$note = $this->find_note_for_product_id( $product_id );
|
||||
if ( $note ) {
|
||||
$note_content_data = $note->get_content_data();
|
||||
if ( $note_content_data->expired ) {
|
||||
// We've already got a full fledged expired note for this. Bail.
|
||||
// Expired notes' content don't change with time.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$note_title = sprintf(
|
||||
/* translators: name of the extension subscription that expired */
|
||||
__( '%s subscription expired', 'woocommerce' ),
|
||||
$product_name
|
||||
);
|
||||
|
||||
$note_content = sprintf(
|
||||
/* translators: date the subscription expired, e.g. Jun 7th 2018 */
|
||||
__( 'Your subscription expired on %s. Get a new subscription to continue receiving updates and access to support.', 'woocommerce' ),
|
||||
$expires_date
|
||||
);
|
||||
|
||||
$note_content_data = (object) array(
|
||||
'product_id' => $product_id,
|
||||
'product_name' => $product_name,
|
||||
'expired' => true,
|
||||
'expires' => $expires,
|
||||
'expires_date' => $expires_date,
|
||||
);
|
||||
|
||||
if ( ! $note ) {
|
||||
$note = new Note();
|
||||
}
|
||||
|
||||
$note->set_title( $note_title );
|
||||
$note->set_content( $note_content );
|
||||
$note->set_content_data( $note_content_data );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_WARNING );
|
||||
$note->set_name( self::SUBSCRIPTION_NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->clear_actions();
|
||||
$note->add_action(
|
||||
'renew-subscription',
|
||||
__( 'Renew Subscription', 'woocommerce' ),
|
||||
$product_page
|
||||
);
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* For each active subscription on this site, checks the expiration date and creates/updates/deletes notes.
|
||||
*/
|
||||
public function refresh_subscription_notes() {
|
||||
if ( ! $this->is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->prune_inactive_subscription_notes();
|
||||
|
||||
$subscriptions = \WC_Helper::get_subscriptions();
|
||||
$active_product_ids = $this->get_subscription_active_product_ids();
|
||||
|
||||
foreach ( (array) $subscriptions as $subscription ) {
|
||||
// Only concern ourselves with active products.
|
||||
$product_id = $subscription['product_id'];
|
||||
if ( ! in_array( $product_id, $active_product_ids, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the subscription will auto-renew, clean up and exit.
|
||||
if ( $subscription['autorenew'] ) {
|
||||
$this->delete_any_note_for_product_id( $product_id );
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the subscription is not expiring by the first threshold, clean up and exit.
|
||||
$bump_thresholds = $this->get_bump_thresholds();
|
||||
$first_threshold = DAY_IN_SECONDS * $bump_thresholds[0];
|
||||
$expires = intval( $subscription['expires'] );
|
||||
$time_now_gmt = current_time( 'timestamp', 0 );
|
||||
if ( $expires > $time_now_gmt + $first_threshold ) {
|
||||
$this->delete_any_note_for_product_id( $product_id );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, if the subscription can still have auto-renew enabled, let them know that now.
|
||||
if ( $expires > $time_now_gmt ) {
|
||||
$this->add_or_update_subscription_expiring( $subscription );
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we got this far, the subscription has completely expired, let them know.
|
||||
$this->add_or_update_subscription_expired( $subscription );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
/**
|
||||
* Initializes backend logic for the onboarding process.
|
||||
*/
|
||||
class Onboarding {
|
||||
/**
|
||||
* Initialize onboarding functionality.
|
||||
*
|
||||
* @internal This method is for internal purposes only.
|
||||
*/
|
||||
final public static function init() {
|
||||
OnboardingHelper::instance()->init();
|
||||
OnboardingIndustries::init();
|
||||
OnboardingJetpack::instance()->init();
|
||||
OnboardingMailchimp::instance()->init();
|
||||
OnboardingProfile::init();
|
||||
OnboardingSetupWizard::instance()->init();
|
||||
OnboardingSync::instance()->init();
|
||||
OnboardingThemes::init();
|
||||
OnboardingFonts::init();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Font\FontFace;
|
||||
use Automattic\WooCommerce\Internal\Font\FontFamily;
|
||||
|
||||
|
||||
/**
|
||||
* Class to install fonts for the Assembler.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class OnboardingFonts {
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*
|
||||
* @internal This method is for internal purposes only.
|
||||
*/
|
||||
final public static function init() {
|
||||
add_action( 'woocommerce_install_assembler_fonts', array( __CLASS__, 'install_fonts' ) );
|
||||
add_filter( 'update_option_woocommerce_allow_tracking', array( self::class, 'start_install_fonts_async_job' ), 10, 2 );
|
||||
}
|
||||
|
||||
const SOURCE_LOGGER = 'font_loader';
|
||||
|
||||
/**
|
||||
* Font families to install.
|
||||
* PHP version of https://github.com/woocommerce/woocommerce/blob/45923dc5f38150c717210ae9db10045cd9582331/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts/#L13-L74
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const FONT_FAMILIES_TO_INSTALL = array(
|
||||
'inter' => array(
|
||||
'fontFamily' => 'Inter',
|
||||
'fontWeights' => array( '400', '500', '600' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'bodoni-moda' => array(
|
||||
'fontFamily' => 'Bodoni Moda',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'overpass' => array(
|
||||
'fontFamily' => 'Overpass',
|
||||
'fontWeights' => array( '300', '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'albert-sans' => array(
|
||||
'fontFamily' => 'Albert Sans',
|
||||
'fontWeights' => array( '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'lora' => array(
|
||||
'fontFamily' => 'Lora',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'montserrat' => array(
|
||||
'fontFamily' => 'Montserrat',
|
||||
'fontWeights' => array( '500', '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'arvo' => array(
|
||||
'fontFamily' => 'Arvo',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'rubik' => array(
|
||||
'fontFamily' => 'Rubik',
|
||||
'fontWeights' => array( '400', '800' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'newsreader' => array(
|
||||
'fontFamily' => 'Newsreader',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'cormorant' => array(
|
||||
'fontFamily' => 'Cormorant',
|
||||
'fontWeights' => array( '400', '500' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'work-sans' => array(
|
||||
'fontFamily' => 'Work Sans',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'raleway' => array(
|
||||
'fontFamily' => 'Raleway',
|
||||
'fontWeights' => array( '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Start install fonts async job.
|
||||
*
|
||||
* @param string $old_value Old option value.
|
||||
* @param string $value Option value.
|
||||
* @return string
|
||||
*/
|
||||
public static function start_install_fonts_async_job( $old_value, $value ) {
|
||||
if ( 'yes' !== $value || ! class_exists( 'WP_Font_Library' ) ) {
|
||||
return;
|
||||
}
|
||||
WC()->call_function(
|
||||
'as_schedule_single_action',
|
||||
WC()->call_function( 'time' ),
|
||||
'woocommerce_install_assembler_fonts',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create Font Families and Font Faces.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_fonts() {
|
||||
$collections = \WP_Font_Library::get_instance()->get_font_collections();
|
||||
$google_fonts = $collections['google-fonts']->get_data();
|
||||
$font_collection = $google_fonts['font_families'];
|
||||
$slug_font_families_to_install = array_keys( self::FONT_FAMILIES_TO_INSTALL );
|
||||
$installed_font_families = self::install_font_families( $slug_font_families_to_install, $font_collection );
|
||||
|
||||
if ( ! empty( $installed_font_families ) ) {
|
||||
$font_faces_from_collection = self::get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection );
|
||||
self::install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Install font families.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $font_collection Font collection.
|
||||
* @return array
|
||||
*/
|
||||
private static function install_font_families( $slug_font_families_to_install, $font_collection ) {
|
||||
return array_reduce(
|
||||
$slug_font_families_to_install,
|
||||
function( $carry, $slug ) use ( $font_collection ) {
|
||||
$font_family_from_collection = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
|
||||
$font_family_name = $font_family_from_collection['fontFamily'];
|
||||
$font_family_installed = FontFamily::get_font_family_by_name( $font_family_name );
|
||||
if ( $font_family_installed ) {
|
||||
return array_merge( $carry, array( $slug => $font_family_installed ) );
|
||||
}
|
||||
|
||||
$font_family_settings = array(
|
||||
'fontFamily' => $font_family_from_collection['fontFamily'],
|
||||
'preview' => $font_family_from_collection['preview'],
|
||||
'slug' => $font_family_from_collection['slug'],
|
||||
'name' => $font_family_from_collection['name'],
|
||||
);
|
||||
|
||||
$font_family_id = FontFamily::insert_font_family( $font_family_settings );
|
||||
if ( is_wp_error( $font_family_id ) ) {
|
||||
if ( 'duplicate_font_family' !== $font_family_id->get_error_code() ) {
|
||||
wc_get_logger()->error(
|
||||
sprintf(
|
||||
'Font Family installation error: %s',
|
||||
$font_family_id->get_error_message(),
|
||||
),
|
||||
array( 'source' => self::SOURCE_LOGGER )
|
||||
);
|
||||
}
|
||||
|
||||
return $carry;
|
||||
}
|
||||
return array_merge( $carry, array( $slug => get_post( $font_family_id ) ) );
|
||||
},
|
||||
array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install font faces.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $installed_font_families Installed font families.
|
||||
* @param array $font_faces_from_collection Font faces from collection.
|
||||
*/
|
||||
private static function install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection ) {
|
||||
foreach ( $slug_font_families_to_install as $slug ) {
|
||||
$font_family = $installed_font_families[ $slug ];
|
||||
$font_faces = $font_faces_from_collection[ $slug ];
|
||||
$font_faces_to_install = self::FONT_FAMILIES_TO_INSTALL[ $slug ]['fontWeights'];
|
||||
|
||||
foreach ( $font_faces as $font_face ) {
|
||||
if ( ! in_array( $font_face['fontWeight'], $font_faces_to_install, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slug = \WP_Font_Utils::get_font_face_slug( $font_face );
|
||||
$font_face_installed = FontFace::get_installed_font_faces_by_slug( $slug );
|
||||
if ( $font_face_installed ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wp_error = FontFace::insert_font_face( $font_face, $font_family->ID );
|
||||
|
||||
if ( is_wp_error( $wp_error ) ) {
|
||||
wc_get_logger()->error(
|
||||
sprintf(
|
||||
/* translators: %s: error message */
|
||||
__( 'Font Face installation error: %s', 'woocommerce' ),
|
||||
$wp_error->get_error_message()
|
||||
),
|
||||
array( 'source' => self::SOURCE_LOGGER )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get font faces data from font collection.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $font_collection Font collection.
|
||||
* @return array
|
||||
*/
|
||||
private static function get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection ) {
|
||||
return array_reduce(
|
||||
$slug_font_families_to_install,
|
||||
function( $carry, $slug ) use ( $font_collection ) {
|
||||
$font_family = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
|
||||
if ( ! $font_family ) {
|
||||
return $carry;
|
||||
}
|
||||
return array_merge( $carry, array( $slug => $font_family['fontFace'] ) );
|
||||
},
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get font family by slug from font collection.
|
||||
*
|
||||
* @param string $slug Font slug.
|
||||
* @param array $font_families_collection Font families collection.
|
||||
* @return array|null
|
||||
*/
|
||||
private static function get_font_family_by_slug_from_font_collection( $slug, $font_families_collection ) {
|
||||
$font_family = null;
|
||||
|
||||
foreach ( $font_families_collection as $font_family ) {
|
||||
if ( $font_family['font_family_settings']['slug'] === $slug ) {
|
||||
$font_family = $font_family['font_family_settings'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $font_family;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Helper
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the onboarding profile and checklist feature.
|
||||
*/
|
||||
class OnboardingHelper {
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var OnboardingHelper instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'current_screen', array( $this, 'add_help_tab' ), 60 );
|
||||
add_action( 'current_screen', array( $this, 'reset_task_list' ) );
|
||||
add_action( 'current_screen', array( $this, 'reset_extended_task_list' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the help tab setup link to reset the onboarding profiler.
|
||||
*/
|
||||
public function add_help_tab() {
|
||||
if ( ! function_exists( 'wc_get_screen_ids' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids(), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the old help tab if it exists.
|
||||
$help_tabs = $screen->get_help_tabs();
|
||||
foreach ( $help_tabs as $help_tab ) {
|
||||
if ( 'woocommerce_onboard_tab' !== $help_tab['id'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$screen->remove_help_tab( 'woocommerce_onboard_tab' );
|
||||
}
|
||||
|
||||
// Add the new help tab.
|
||||
$help_tab = array(
|
||||
'title' => __( 'Setup wizard', 'woocommerce' ),
|
||||
'id' => 'woocommerce_onboard_tab',
|
||||
);
|
||||
|
||||
$setup_list = TaskLists::get_list( 'setup' );
|
||||
$extended_list = TaskLists::get_list( 'extended' );
|
||||
|
||||
if ( $setup_list ) {
|
||||
$help_tab['content'] = '<h2>' . __( 'WooCommerce Onboarding', 'woocommerce' ) . '</h2>';
|
||||
|
||||
$help_tab['content'] .= '<h3>' . __( 'Profile Setup Wizard', 'woocommerce' ) . '</h3>';
|
||||
$help_tab['content'] .= '<p>' . __( 'If you need to access the setup wizard again, please click on the button below.', 'woocommerce' ) . '</p>' .
|
||||
'<p><a href="' . wc_admin_url( '&path=/setup-wizard' ) . '" class="button button-primary">' . __( 'Setup wizard', 'woocommerce' ) . '</a></p>';
|
||||
|
||||
$help_tab['content'] .= '<h3>' . __( 'Task List', 'woocommerce' ) . '</h3>';
|
||||
$help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the task lists, please click on the button below.', 'woocommerce' ) . '</p>' .
|
||||
( $setup_list->is_hidden()
|
||||
? '<p><a href="' . wc_admin_url( '&reset_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>'
|
||||
: '<p><a href="' . wc_admin_url( '&reset_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $extended_list ) {
|
||||
$help_tab['content'] .= '<h3>' . __( 'Extended task List', 'woocommerce' ) . '</h3>';
|
||||
$help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the extended task lists, please click on the button below.', 'woocommerce' ) . '</p>' .
|
||||
( $extended_list->is_hidden()
|
||||
? '<p><a href="' . wc_admin_url( '&reset_extended_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>'
|
||||
: '<p><a href="' . wc_admin_url( '&reset_extended_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>'
|
||||
);
|
||||
}
|
||||
|
||||
$screen->add_help_tab( $help_tab );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the onboarding task list and redirect to the dashboard.
|
||||
*/
|
||||
public function reset_task_list() {
|
||||
if (
|
||||
! PageController::is_admin_page() ||
|
||||
! isset( $_GET['reset_task_list'] ) // phpcs:ignore CSRF ok.
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task_list = TaskLists::get_list( 'setup' );
|
||||
|
||||
if ( ! $task_list ) {
|
||||
return;
|
||||
}
|
||||
$show = 1 === absint( $_GET['reset_task_list'] ); // phpcs:ignore CSRF ok.
|
||||
$update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok.
|
||||
|
||||
if ( $update ) {
|
||||
wc_admin_record_tracks_event(
|
||||
'tasklist_toggled',
|
||||
array(
|
||||
'status' => $show ? 'enabled' : 'disabled',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
wp_safe_redirect( wc_admin_url() );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the extended task list and redirect to the dashboard.
|
||||
*/
|
||||
public function reset_extended_task_list() {
|
||||
if (
|
||||
! PageController::is_admin_page() ||
|
||||
! isset( $_GET['reset_extended_task_list'] ) // phpcs:ignore CSRF ok.
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task_list = TaskLists::get_list( 'extended' );
|
||||
|
||||
if ( ! $task_list ) {
|
||||
return;
|
||||
}
|
||||
$show = 1 === absint( $_GET['reset_extended_task_list'] ); // phpcs:ignore CSRF ok.
|
||||
$update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok.
|
||||
|
||||
if ( $update ) {
|
||||
wc_admin_record_tracks_event(
|
||||
'extended_tasklist_toggled',
|
||||
array(
|
||||
'status' => $show ? 'disabled' : 'enabled',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
wp_safe_redirect( wc_admin_url() );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Industries
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
/**
|
||||
* Logic around onboarding industries.
|
||||
*/
|
||||
class OnboardingIndustries {
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of allowed industries for the onboarding wizard.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_allowed_industries() {
|
||||
/* With "use_description" we turn the description input on. With "description_label" we set the input label */
|
||||
return apply_filters(
|
||||
'woocommerce_admin_onboarding_industries',
|
||||
array(
|
||||
'fashion-apparel-accessories' => array(
|
||||
'label' => __( 'Fashion, apparel, and accessories', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'health-beauty' => array(
|
||||
'label' => __( 'Health and beauty', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'electronics-computers' => array(
|
||||
'label' => __( 'Electronics and computers', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'food-drink' => array(
|
||||
'label' => __( 'Food and drink', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'home-furniture-garden' => array(
|
||||
'label' => __( 'Home, furniture, and garden', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'cbd-other-hemp-derived-products' => array(
|
||||
'label' => __( 'CBD and other hemp-derived products', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'education-and-learning' => array(
|
||||
'label' => __( 'Education and learning', 'woocommerce' ),
|
||||
'use_description' => false,
|
||||
'description_label' => '',
|
||||
),
|
||||
'other' => array(
|
||||
'label' => __( 'Other', 'woocommerce' ),
|
||||
'use_description' => true,
|
||||
'description_label' => __( 'Description', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add preloaded data to onboarding.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
* @return array
|
||||
*/
|
||||
public static function preload_data( $settings ) {
|
||||
$settings['onboarding']['industries'] = self::get_allowed_industries();
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Jetpack
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
/**
|
||||
* Contains logic around Jetpack setup during onboarding.
|
||||
*/
|
||||
class OnboardingJetpack {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var OnboardingJetpack instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'woocommerce_admin_plugins_pre_activate', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) );
|
||||
add_action( 'woocommerce_admin_plugins_pre_install', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) );
|
||||
|
||||
// Always hook into Jetpack connection even if outside of admin.
|
||||
add_action( 'jetpack_site_registered', array( $this, 'set_woocommerce_setup_jetpack_opted_in' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the woocommerce_setup_jetpack_opted_in to true when Jetpack connects to WPCOM.
|
||||
*/
|
||||
public function set_woocommerce_setup_jetpack_opted_in() {
|
||||
update_option( 'woocommerce_setup_jetpack_opted_in', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that Jetpack gets installed and activated ahead of WooCommerce Payments
|
||||
* if both are being installed/activated at the same time.
|
||||
*
|
||||
* See: https://github.com/Automattic/woocommerce-payments/issues/1663
|
||||
* See: https://github.com/Automattic/jetpack/issues/19624
|
||||
*
|
||||
* @param array $plugins A list of plugins to install or activate.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function activate_and_install_jetpack_ahead_of_wcpay( $plugins ) {
|
||||
if ( in_array( 'jetpack', $plugins, true ) && in_array( 'woocommerce-payments', $plugins, true ) ) {
|
||||
array_unshift( $plugins, 'jetpack' );
|
||||
$plugins = array_unique( $plugins );
|
||||
}
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Mailchimp
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler;
|
||||
|
||||
/**
|
||||
* Logic around updating Mailchimp during onboarding.
|
||||
*/
|
||||
class OnboardingMailchimp {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var OnboardingMailchimp instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'woocommerce_onboarding_profile_data_updated', array( $this, 'on_profile_data_updated' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset MailchimpScheduler if profile data is being updated with a new email.
|
||||
*
|
||||
* @param array $existing_data Existing option data.
|
||||
* @param array $updating_data Updating option data.
|
||||
*/
|
||||
public function on_profile_data_updated( $existing_data, $updating_data ) {
|
||||
if (
|
||||
isset( $existing_data['store_email'] ) &&
|
||||
isset( $updating_data['store_email'] ) &&
|
||||
$existing_data['store_email'] !== $updating_data['store_email']
|
||||
) {
|
||||
MailchimpScheduler::reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Products
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Loader;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
|
||||
/**
|
||||
* Class for handling product types and data around product types.
|
||||
*/
|
||||
class OnboardingProducts {
|
||||
|
||||
/**
|
||||
* Name of product data transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PRODUCT_DATA_TRANSIENT = 'wc_onboarding_product_data';
|
||||
|
||||
/**
|
||||
* Get a list of allowed product types for the onboarding wizard.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_allowed_product_types() {
|
||||
$products = array(
|
||||
'physical' => array(
|
||||
'label' => __( 'Physical products', 'woocommerce' ),
|
||||
'default' => true,
|
||||
),
|
||||
'downloads' => array(
|
||||
'label' => __( 'Downloads', 'woocommerce' ),
|
||||
),
|
||||
'subscriptions' => array(
|
||||
'label' => __( 'Subscriptions', 'woocommerce' ),
|
||||
),
|
||||
'memberships' => array(
|
||||
'label' => __( 'Memberships', 'woocommerce' ),
|
||||
'product' => 958589,
|
||||
),
|
||||
'bookings' => array(
|
||||
'label' => __( 'Bookings', 'woocommerce' ),
|
||||
'product' => 390890,
|
||||
),
|
||||
'product-bundles' => array(
|
||||
'label' => __( 'Bundles', 'woocommerce' ),
|
||||
'product' => 18716,
|
||||
),
|
||||
'product-add-ons' => array(
|
||||
'label' => __( 'Customizable products', 'woocommerce' ),
|
||||
'product' => 18618,
|
||||
),
|
||||
);
|
||||
$base_location = wc_get_base_location();
|
||||
$has_cbd_industry = false;
|
||||
if ( 'US' === $base_location['country'] ) {
|
||||
$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
if ( ! empty( $profile['industry'] ) ) {
|
||||
$has_cbd_industry = in_array( 'cbd-other-hemp-derived-products', array_column( $profile['industry'], 'slug' ), true );
|
||||
}
|
||||
}
|
||||
if ( ! Features::is_enabled( 'subscriptions' ) || 'US' !== $base_location['country'] || $has_cbd_industry ) {
|
||||
$products['subscriptions']['product'] = 27147;
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_admin_onboarding_product_types', $products );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dynamic product data from API.
|
||||
*
|
||||
* @param array $product_types Array of product types.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product_data( $product_types ) {
|
||||
$locale = get_user_locale();
|
||||
// Transient value is an array of product data keyed by locale.
|
||||
$transient_value = get_transient( self::PRODUCT_DATA_TRANSIENT );
|
||||
$transient_value = is_array( $transient_value ) ? $transient_value : array();
|
||||
$woocommerce_products = $transient_value[ $locale ] ?? false;
|
||||
|
||||
if ( false === $woocommerce_products ) {
|
||||
$woocommerce_products = wp_remote_get(
|
||||
add_query_arg(
|
||||
array(
|
||||
'locale' => $locale,
|
||||
),
|
||||
'https://woocommerce.com/wp-json/wccom-extensions/1.0/search'
|
||||
),
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
if ( is_wp_error( $woocommerce_products ) ) {
|
||||
return $product_types;
|
||||
}
|
||||
$transient_value[ $locale ] = $woocommerce_products;
|
||||
set_transient( self::PRODUCT_DATA_TRANSIENT, $transient_value, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
$data = json_decode( $woocommerce_products['body'] );
|
||||
$products = array();
|
||||
$product_data = array();
|
||||
|
||||
// Map product data by ID.
|
||||
if ( isset( $data ) && isset( $data->products ) ) {
|
||||
foreach ( $data->products as $product_datum ) {
|
||||
if ( isset( $product_datum->id ) ) {
|
||||
$products[ $product_datum->id ] = $product_datum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over product types and append data.
|
||||
foreach ( $product_types as $key => $product_type ) {
|
||||
$product_data[ $key ] = $product_types[ $key ];
|
||||
|
||||
if ( isset( $product_type['product'] ) && isset( $products[ $product_type['product'] ] ) ) {
|
||||
$price = html_entity_decode( $products[ $product_type['product'] ]->price );
|
||||
$yearly_price = (float) str_replace( '$', '', $price );
|
||||
|
||||
$product_data[ $key ]['yearly_price'] = $yearly_price;
|
||||
$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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $product_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed product types with the polled data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product_types_with_data() {
|
||||
return self::get_product_data( self::get_allowed_product_types() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relevant purchaseable products for the site.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_relevant_products() {
|
||||
$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
$installed = PluginsHelper::get_installed_plugin_slugs();
|
||||
$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();
|
||||
$product_data = self::get_product_types_with_data();
|
||||
$purchaseable = array();
|
||||
$remaining = array();
|
||||
foreach ( $product_types as $type ) {
|
||||
if ( ! isset( $product_data[ $type ]['slug'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$purchaseable[] = $product_data[ $type ];
|
||||
|
||||
if ( ! in_array( $product_data[ $type ]['slug'], $installed, true ) ) {
|
||||
$remaining[] = $product_data[ $type ]['label'];
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'purchaseable' => $purchaseable,
|
||||
'remaining' => $remaining,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Setup Wizard
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the onboarding profile and checklist feature.
|
||||
*/
|
||||
class OnboardingProfile {
|
||||
/**
|
||||
* Profile data option name.
|
||||
*/
|
||||
const DATA_OPTION = 'woocommerce_onboarding_profile';
|
||||
|
||||
/**
|
||||
* Add onboarding actions.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'update_option_' . self::DATA_OPTION, array( __CLASS__, 'trigger_complete' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the woocommerce_onboarding_profile_completed action
|
||||
*
|
||||
* @param array $old_value Previous value.
|
||||
* @param array $value Current value.
|
||||
*/
|
||||
public static function trigger_complete( $old_value, $value ) {
|
||||
if ( isset( $old_value['completed'] ) && $old_value['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action hook fired when the onboarding profile (or onboarding wizard,
|
||||
* or profiler) is completed.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
do_action( 'woocommerce_onboarding_profile_completed' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the profiler still needs to be completed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function needs_completion() {
|
||||
$onboarding_data = get_option( self::DATA_OPTION, array() );
|
||||
|
||||
$is_completed = isset( $onboarding_data['completed'] ) && true === $onboarding_data['completed'];
|
||||
$is_skipped = isset( $onboarding_data['skipped'] ) && true === $onboarding_data['skipped'];
|
||||
|
||||
// @todo When merging to WooCommerce Core, we should set the `completed` flag to true during the upgrade progress.
|
||||
// https://github.com/woocommerce/woocommerce-admin/pull/2300#discussion_r287237498.
|
||||
return ! $is_completed && ! $is_skipped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Setup Wizard
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the onboarding profile and checklist feature.
|
||||
*/
|
||||
class OnboardingSetupWizard {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var OnboardingSetupWizard instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add onboarding actions.
|
||||
*/
|
||||
public function init() {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Old settings injection.
|
||||
// Run after Automattic\WooCommerce\Internal\Admin\Loader.
|
||||
add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 );
|
||||
// New settings injection.
|
||||
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 );
|
||||
add_filter( 'woocommerce_admin_preload_settings', array( $this, 'preload_settings' ) );
|
||||
add_filter( 'admin_body_class', array( $this, 'add_loading_classes' ) );
|
||||
add_action( 'admin_init', array( $this, 'do_admin_redirects' ) );
|
||||
add_action( 'current_screen', array( $this, 'redirect_to_profiler' ) );
|
||||
add_filter( 'woocommerce_show_admin_notice', array( $this, 'remove_old_install_notice' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the context of execution comes from async action scheduler.
|
||||
* Note: this is a polyfill for wc_is_running_from_async_action_scheduler()
|
||||
* which was introduced in WC 4.0.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_running_from_async_action_scheduler() {
|
||||
if ( function_exists( '\wc_is_running_from_async_action_scheduler' ) ) {
|
||||
return \wc_is_running_from_async_action_scheduler();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirects to setup/welcome page after install and updates.
|
||||
*
|
||||
* For setup wizard, transient must be present, the user must have access rights, and we must ignore the network/bulk plugin updaters.
|
||||
*/
|
||||
public function do_admin_redirects() {
|
||||
// Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient.
|
||||
// That means OBW would never be shown.
|
||||
if ( $this->is_running_from_async_action_scheduler() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup wizard redirect.
|
||||
if ( get_transient( '_wc_activation_redirect' ) && apply_filters( 'woocommerce_enable_setup_wizard', true ) ) {
|
||||
$do_redirect = true;
|
||||
$current_page = isset( $_GET['page'] ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$is_onboarding_path = ! isset( $_GET['path'] ) || '/setup-wizard' === wc_clean( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
|
||||
|
||||
// On these pages, or during these events, postpone the redirect.
|
||||
if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
$do_redirect = false;
|
||||
}
|
||||
|
||||
// On these pages, or during these events, disable the redirect.
|
||||
if (
|
||||
( 'wc-admin' === $current_page && $is_onboarding_path ) ||
|
||||
apply_filters( 'woocommerce_prevent_automatic_wizard_redirect', false ) ||
|
||||
isset( $_GET['activate-multi'] ) // phpcs:ignore WordPress.Security.NonceVerification
|
||||
) {
|
||||
delete_transient( '_wc_activation_redirect' );
|
||||
$do_redirect = false;
|
||||
}
|
||||
|
||||
if ( $do_redirect ) {
|
||||
delete_transient( '_wc_activation_redirect' );
|
||||
wp_safe_redirect( wc_admin_url() );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the woocommerce_onboarding_profile_completed action
|
||||
*
|
||||
* @param array $old_value Previous value.
|
||||
* @param array $value Current value.
|
||||
*/
|
||||
public function trigger_profile_completed_action( $old_value, $value ) {
|
||||
if ( isset( $old_value['completed'] ) && $old_value['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action hook fired when the onboarding profile (or onboarding wizard,
|
||||
* or profiler) is completed.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
do_action( 'woocommerce_onboarding_profile_completed' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the profiler should be displayed (not completed and not skipped).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_show() {
|
||||
if ( $this->is_setup_wizard() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return OnboardingProfile::needs_completion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the profiler on homepage if completion is needed.
|
||||
*/
|
||||
public function redirect_to_profiler() {
|
||||
if ( ! $this->is_homepage() || ! OnboardingProfile::needs_completion() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( wc_admin_url( '&path=/setup-wizard' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the profile wizard.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_setup_wizard() {
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
return isset( $_GET['page'] ) &&
|
||||
'wc-admin' === $_GET['page'] &&
|
||||
isset( $_GET['path'] ) &&
|
||||
'/setup-wizard' === $_GET['path'];
|
||||
/* phpcs: enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the homepage.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_homepage() {
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
return isset( $_GET['page'] ) &&
|
||||
'wc-admin' === $_GET['page'] &&
|
||||
! isset( $_GET['path'] );
|
||||
/* phpcs: enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current page is one of the WC Admin pages.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_woocommerce_page() {
|
||||
$current_page = PageController::get_instance()->get_current_page();
|
||||
if ( ! $current_page || ! isset( $current_page['path'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 0 === strpos( $current_page['path'], 'wc-admin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add profiler items to component settings.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function component_settings( $settings ) {
|
||||
$profile = (array) get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
$settings['onboarding'] = array(
|
||||
'profile' => $profile,
|
||||
);
|
||||
|
||||
// Only fetch if the onboarding wizard OR the task list is incomplete or currently shown
|
||||
// or the current page is one of the WooCommerce Admin pages.
|
||||
if (
|
||||
( ! $this->should_show() && ! count( TaskLists::get_visible() )
|
||||
||
|
||||
! $this->is_woocommerce_page()
|
||||
)
|
||||
) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
|
||||
$wccom_auth = \WC_Helper_Options::get( 'auth' );
|
||||
$profile['wccom_connected'] = empty( $wccom_auth['access_token'] ) ? false : true;
|
||||
|
||||
$settings['onboarding']['currencySymbols'] = get_woocommerce_currency_symbols();
|
||||
$settings['onboarding']['euCountries'] = WC()->countries->get_european_union_countries();
|
||||
$settings['onboarding']['localeInfo'] = include WC()->plugin_path() . '/i18n/locale-info.php';
|
||||
$settings['onboarding']['profile'] = $profile;
|
||||
|
||||
if ( $this->is_setup_wizard() ) {
|
||||
$settings['onboarding']['pageCount'] = (int) ( wp_count_posts( 'page' ) )->publish;
|
||||
$settings['onboarding']['postCount'] = (int) ( wp_count_posts( 'post' ) )->publish;
|
||||
$settings['onboarding']['isBlockTheme'] = wc_current_theme_is_fse_theme();
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_admin_onboarding_preloaded_data', $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload WC setting options to prime state of the application.
|
||||
*
|
||||
* @param array $options Array of options to preload.
|
||||
* @return array
|
||||
*/
|
||||
public function preload_settings( $options ) {
|
||||
$options[] = 'general';
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the admin full screen class when loading to prevent flashes of unstyled content.
|
||||
*
|
||||
* @param bool $classes Body classes.
|
||||
* @return array
|
||||
*/
|
||||
public function add_loading_classes( $classes ) {
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
if ( $this->is_setup_wizard() ) {
|
||||
$classes .= ' woocommerce-admin-full-screen';
|
||||
}
|
||||
/* phpcs: enable */
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the install notice that prompts the user to visit the old onboarding setup wizard.
|
||||
*
|
||||
* @param bool $show Show or hide the notice.
|
||||
* @param string $notice The slug of the notice.
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_old_install_notice( $show, $notice ) {
|
||||
if ( 'install' === $notice ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $show;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the onboarding profile and checklist feature.
|
||||
*/
|
||||
class OnboardingSync {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var OnboardingSync instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( $this, 'send_profile_data_on_update' ), 10, 2 );
|
||||
add_action( 'woocommerce_helper_connected', array( $this, 'send_profile_data_on_connect' ) );
|
||||
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'current_screen', array( $this, 'redirect_wccom_install' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send profile data to WooCommerce.com.
|
||||
*/
|
||||
private function send_profile_data() {
|
||||
if ( 'yes' !== get_option( 'woocommerce_allow_tracking', 'no' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! class_exists( '\WC_Helper_API' ) || ! method_exists( '\WC_Helper_API', 'put' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! class_exists( '\WC_Helper_Options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$auth = \WC_Helper_Options::get( 'auth' );
|
||||
if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
$base_location = wc_get_base_location();
|
||||
$defaults = array(
|
||||
'plugins' => 'skipped',
|
||||
'industry' => array(),
|
||||
'product_types' => array(),
|
||||
'product_count' => '0',
|
||||
'selling_venues' => 'no',
|
||||
'number_employees' => '1',
|
||||
'revenue' => 'none',
|
||||
'other_platform' => 'none',
|
||||
'business_extensions' => array(),
|
||||
'theme' => get_stylesheet(),
|
||||
'setup_client' => false,
|
||||
'store_location' => $base_location['country'],
|
||||
'default_currency' => get_woocommerce_currency(),
|
||||
);
|
||||
|
||||
// Prepare industries as an array of slugs if they are in array format.
|
||||
if ( isset( $profile['industry'] ) && is_array( $profile['industry'] ) ) {
|
||||
$industry_slugs = array();
|
||||
foreach ( $profile['industry'] as $industry ) {
|
||||
$industry_slugs[] = is_array( $industry ) ? $industry['slug'] : $industry;
|
||||
}
|
||||
$profile['industry'] = $industry_slugs;
|
||||
}
|
||||
$body = wp_parse_args( $profile, $defaults );
|
||||
|
||||
\WC_Helper_API::put(
|
||||
'profile',
|
||||
array(
|
||||
'authenticated' => true,
|
||||
'body' => wp_json_encode( $body ),
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send profiler data on profiler change to completion.
|
||||
*
|
||||
* @param array $old_value Previous value.
|
||||
* @param array $value Current value.
|
||||
*/
|
||||
public function send_profile_data_on_update( $old_value, $value ) {
|
||||
if ( ! isset( $value['completed'] ) || ! $value['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->send_profile_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send profiler data after a site is connected.
|
||||
*/
|
||||
public function send_profile_data_on_connect() {
|
||||
$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
if ( ! isset( $profile['completed'] ) || ! $profile['completed'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->send_profile_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the task list if the task list is enabled and finishing a wccom checkout.
|
||||
*
|
||||
* @todo Once URL params are added to the redirect, we can check those instead of the referer.
|
||||
*/
|
||||
public function redirect_wccom_install() {
|
||||
$task_list = TaskLists::get_list( 'setup' );
|
||||
|
||||
if (
|
||||
! $task_list ||
|
||||
$task_list->is_hidden() ||
|
||||
! isset( $_SERVER['HTTP_REFERER'] ) ||
|
||||
0 !== strpos( wp_unslash( $_SERVER['HTTP_REFERER'] ), 'https://woocommerce.com/checkout?utm_medium=product' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( wc_admin_url() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Onboarding Themes
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Loader;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Init as OnboardingTasks;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;
|
||||
|
||||
/**
|
||||
* Logic around onboarding themes.
|
||||
*/
|
||||
class OnboardingThemes {
|
||||
/**
|
||||
* Name of themes transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const THEMES_TRANSIENT = 'wc_onboarding_themes';
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_theme_installed', array( __CLASS__, 'delete_themes_transient' ) );
|
||||
add_action( 'after_switch_theme', array( __CLASS__, 'delete_themes_transient' ) );
|
||||
add_filter( 'woocommerce_rest_prepare_themes', array( __CLASS__, 'add_uploaded_theme_data' ) );
|
||||
add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get puchasable theme by slug.
|
||||
*
|
||||
* @param string $price_string string of price.
|
||||
* @return float|null
|
||||
*/
|
||||
private static function get_price_from_string( $price_string ) {
|
||||
$price_match = null;
|
||||
// Parse price from string as it includes the currency symbol.
|
||||
preg_match( '/\\d+\.\d{2}\s*/', $price_string, $price_match );
|
||||
if ( count( $price_match ) > 0 ) {
|
||||
return (float) $price_match[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get puchasable theme by slug.
|
||||
*
|
||||
* @param string $slug from theme.
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_paid_theme_by_slug( $slug ) {
|
||||
$themes = self::get_themes();
|
||||
$theme_key = array_search( $slug, array_column( $themes, 'slug' ), true );
|
||||
$theme = false !== $theme_key ? $themes[ $theme_key ] : null;
|
||||
if ( $theme && isset( $theme['id'] ) && isset( $theme['price'] ) ) {
|
||||
$price = self::get_price_from_string( $theme['price'] );
|
||||
if ( $price && $price > 0 ) {
|
||||
return $themes[ $theme_key ];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort themes returned from WooCommerce.com
|
||||
*
|
||||
* @param array $themes Array of themes from WooCommerce.com.
|
||||
* @return array
|
||||
*/
|
||||
public static function sort_woocommerce_themes( $themes ) {
|
||||
usort(
|
||||
$themes,
|
||||
function ( $product_1, $product_2 ) {
|
||||
if ( ! property_exists( $product_1, 'id' ) || ! property_exists( $product_1, 'slug' ) ) {
|
||||
return 1;
|
||||
}
|
||||
if ( ! property_exists( $product_2, 'id' ) || ! property_exists( $product_2, 'slug' ) ) {
|
||||
return 1;
|
||||
}
|
||||
if ( in_array( 'Storefront', array( $product_1->slug, $product_2->slug ), true ) ) {
|
||||
return 'Storefront' === $product_1->slug ? -1 : 1;
|
||||
}
|
||||
return $product_1->id < $product_2->id ? 1 : -1;
|
||||
}
|
||||
);
|
||||
return $themes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of themes for the onboarding wizard.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_themes() {
|
||||
$themes = get_transient( self::THEMES_TRANSIENT );
|
||||
if ( false === $themes ) {
|
||||
$theme_data = wp_remote_get(
|
||||
'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=themes',
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$themes = array();
|
||||
|
||||
if ( ! is_wp_error( $theme_data ) ) {
|
||||
$theme_data = json_decode( $theme_data['body'] );
|
||||
$woo_themes = property_exists( $theme_data, 'products' ) ? $theme_data->products : array();
|
||||
$sorted_themes = self::sort_woocommerce_themes( $woo_themes );
|
||||
|
||||
foreach ( $sorted_themes as $theme ) {
|
||||
$slug = sanitize_title_with_dashes( $theme->slug );
|
||||
$themes[ $slug ] = (array) $theme;
|
||||
$themes[ $slug ]['is_installed'] = false;
|
||||
$themes[ $slug ]['has_woocommerce_support'] = true;
|
||||
$themes[ $slug ]['slug'] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$installed_themes = wp_get_themes();
|
||||
foreach ( $installed_themes as $slug => $theme ) {
|
||||
$theme_data = self::get_theme_data( $theme );
|
||||
if ( isset( $themes[ $slug ] ) ) {
|
||||
$themes[ $slug ]['is_installed'] = true;
|
||||
$themes[ $slug ]['image'] = $theme_data['image'];
|
||||
} else {
|
||||
$themes[ $slug ] = $theme_data;
|
||||
}
|
||||
}
|
||||
|
||||
$active_theme = get_option( 'stylesheet' );
|
||||
|
||||
/**
|
||||
* The active theme may no be set if active_theme is not compatible with current version of WordPress.
|
||||
* In this case, we should not add active theme to onboarding themes.
|
||||
*/
|
||||
if ( isset( $themes[ $active_theme ] ) ) {
|
||||
// Add the WooCommerce support tag for default themes that don't explicitly declare support.
|
||||
if ( function_exists( 'wc_is_wp_default_theme_active' ) && wc_is_wp_default_theme_active() ) {
|
||||
$themes[ $active_theme ]['has_woocommerce_support'] = true;
|
||||
}
|
||||
|
||||
$themes = array( $active_theme => $themes[ $active_theme ] ) + $themes;
|
||||
}
|
||||
|
||||
set_transient( self::THEMES_TRANSIENT, $themes, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
$themes = apply_filters( 'woocommerce_admin_onboarding_themes', $themes );
|
||||
return array_values( $themes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme data used in onboarding theme browser.
|
||||
*
|
||||
* @param WP_Theme $theme Theme to gather data from.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_theme_data( $theme ) {
|
||||
return array(
|
||||
'slug' => sanitize_text_field( $theme->stylesheet ),
|
||||
'title' => $theme->get( 'Name' ),
|
||||
'price' => '0.00',
|
||||
'is_installed' => true,
|
||||
'image' => $theme->get_screenshot(),
|
||||
'has_woocommerce_support' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add theme data to response from themes controller.
|
||||
*
|
||||
* @param WP_REST_Response $response Rest response.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function add_uploaded_theme_data( $response ) {
|
||||
if ( ! isset( $response->data['theme'] ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$theme = wp_get_theme( $response->data['theme'] );
|
||||
$response->data['theme_data'] = self::get_theme_data( $theme );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the stored themes transient.
|
||||
*/
|
||||
public static function delete_themes_transient() {
|
||||
delete_transient( self::THEMES_TRANSIENT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add preloaded data to onboarding.
|
||||
*
|
||||
* @param array $settings Component settings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function preload_data( $settings ) {
|
||||
$settings['onboarding']['activeTheme'] = get_option( 'stylesheet' );
|
||||
$settings['onboarding']['themes'] = self::get_themes();
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of themes that can be installed & activated via the onboarding wizard.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_allowed_themes() {
|
||||
$allowed_themes = array();
|
||||
$themes = self::get_themes();
|
||||
|
||||
foreach ( $themes as $theme ) {
|
||||
$price = preg_replace( '/&#?[a-z0-9]+;/i', '', $theme['price'] );
|
||||
|
||||
if ( $theme['is_installed'] || '0.00' === $price ) {
|
||||
$allowed_themes[] = $theme['slug'];
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_admin_onboarding_themes_whitelist', $allowed_themes );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* When Custom Order Tables are not the default order store (ie, posts are authoritative), we should take care of
|
||||
* redirecting requests for the order editor and order admin list table to the equivalent posts-table screens.
|
||||
*
|
||||
* If the redirect logic is problematic, it can be unhooked using code like the following example:
|
||||
*
|
||||
* remove_action(
|
||||
* 'admin_page_access_denied',
|
||||
* array( wc_get_container()->get( COTRedirectionController::class ), 'handle_hpos_admin_requests' )
|
||||
* );
|
||||
*/
|
||||
class COTRedirectionController {
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Add hooks needed to perform our magic.
|
||||
*/
|
||||
public function setup(): void {
|
||||
// Only take action in cases where access to the admin screen would otherwise be denied.
|
||||
self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially
|
||||
* redirect the user to the equivalent CPT-driven screens.
|
||||
*
|
||||
* @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used.
|
||||
*/
|
||||
private function handle_hpos_admin_requests( $query_params = null ) {
|
||||
$query_params = is_array( $query_params ) ? $query_params : $_GET;
|
||||
|
||||
if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = wp_unslash( $query_params );
|
||||
$action = $params['action'] ?? '';
|
||||
unset( $params['page'] );
|
||||
|
||||
if ( 'edit' === $action && isset( $params['id'] ) ) {
|
||||
$params['post'] = $params['id'];
|
||||
unset( $params['id'] );
|
||||
$new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) );
|
||||
} elseif ( 'new' === $action ) {
|
||||
unset( $params['action'] );
|
||||
$params['post_type'] = 'shop_order';
|
||||
$new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) );
|
||||
} else {
|
||||
// If nonce parameters are present and valid, rebuild them for the CPT admin list table.
|
||||
if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) {
|
||||
$params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' );
|
||||
$params['_wpnonce'] = wp_create_nonce( 'bulk-posts' );
|
||||
}
|
||||
|
||||
// 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';
|
||||
$new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
/**
|
||||
* Renders order edit page, works with both post and order object.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\OrderAttribution;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class Edit.
|
||||
*/
|
||||
class Edit {
|
||||
|
||||
/**
|
||||
* Screen ID for the edit order screen.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $screen_id;
|
||||
|
||||
/**
|
||||
* Instance of the CustomMetaBox class. Used to render meta box for custom meta.
|
||||
*
|
||||
* @var CustomMetaBox
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @var \WC_Order
|
||||
*/
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* Action name that the form is currently handling. Could be new_order or edit_order.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $current_action;
|
||||
|
||||
/**
|
||||
* Message to be displayed to the user. Index of message from the messages array registered when declaring shop_order post type.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param string $screen_id Screen ID.
|
||||
* @param string $title Title of the page.
|
||||
*/
|
||||
public static function add_order_meta_boxes( string $screen_id, string $title ) {
|
||||
/* Translators: %s order type name. */
|
||||
add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Data::output', $screen_id, 'normal', 'high' );
|
||||
add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $screen_id, 'normal', 'high' );
|
||||
/* Translators: %s order type name. */
|
||||
add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Notes::output', $screen_id, 'side', 'default' );
|
||||
add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $screen_id, 'normal', 'default' );
|
||||
/* Translators: %s order type name. */
|
||||
add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Actions::output', $screen_id, 'side', 'high' );
|
||||
self::maybe_register_order_attribution( $screen_id, $title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks metabox save functions for order edit page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_save_meta_boxes() {
|
||||
/**
|
||||
* Save Order Meta Boxes.
|
||||
*
|
||||
* In order:
|
||||
* Save the order items.
|
||||
* Save the order totals.
|
||||
* Save the order downloads.
|
||||
* Save order data - also updates status and sends out admin emails if needed. Last to show latest data.
|
||||
* Save actions - sends out other emails. Last to show latest data.
|
||||
*/
|
||||
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 );
|
||||
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 );
|
||||
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 );
|
||||
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue necessary scripts for order edit page.
|
||||
*/
|
||||
private function enqueue_scripts() {
|
||||
if ( wp_is_mobile() ) {
|
||||
wp_enqueue_script( 'jquery-touch-punch' );
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function setup( \WC_Order $order ) {
|
||||
$this->order = $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.
|
||||
*
|
||||
* Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here.
|
||||
*
|
||||
* @since 3.8.0.
|
||||
*/
|
||||
do_action( 'add_meta_boxes', $this->screen_id, $this->order );
|
||||
|
||||
/**
|
||||
* Provides an opportunity to inject custom meta boxes into the order editor screen. This
|
||||
* hook is an analog of `add_meta_boxes_<POST_TYPE>` as provided by WordPress core.
|
||||
*
|
||||
* @since 7.4.0
|
||||
*
|
||||
* @oaram WC_Order $order The order being edited.
|
||||
*/
|
||||
do_action( 'add_meta_boxes_' . $this->screen_id, $this->order );
|
||||
|
||||
$this->enqueue_scripts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current action for the form.
|
||||
*
|
||||
* @param string $action Action name.
|
||||
*/
|
||||
public function set_current_action( string $action ) {
|
||||
$this->current_action = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks meta box for order specific meta.
|
||||
*/
|
||||
private function add_order_specific_meta_box() {
|
||||
add_meta_box(
|
||||
'order_custom',
|
||||
__( 'Custom Fields', 'woocommerce' ),
|
||||
array( $this, 'render_custom_meta_box' ),
|
||||
$this->screen_id,
|
||||
'normal'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register order attribution meta boxes if the feature is enabled.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $screen_id Screen ID.
|
||||
* @param string $title Title of the page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function maybe_register_order_attribution( string $screen_id, string $title ) {
|
||||
/**
|
||||
* Features controller.
|
||||
*
|
||||
* @var FeaturesController $feature_controller
|
||||
*/
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
if ( ! $feature_controller->feature_is_enabled( 'order_attribution' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order attribution meta box.
|
||||
*
|
||||
* @var OrderAttribution $order_attribution_meta_box
|
||||
*/
|
||||
$order_attribution_meta_box = wc_get_container()->get( OrderAttribution::class );
|
||||
|
||||
add_meta_box(
|
||||
'woocommerce-order-source-data',
|
||||
/* Translators: %s order type name. */
|
||||
sprintf( __( '%s attribution', 'woocommerce' ), $title ),
|
||||
function( $post_or_order ) use ( $order_attribution_meta_box ) {
|
||||
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$order_attribution_meta_box->output( $order );
|
||||
}
|
||||
},
|
||||
$screen_id,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
// Add customer history meta box if analytics is enabled.
|
||||
if ( 'yes' !== get_option( 'woocommerce_analytics_enabled' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! OrderUtil::is_order_edit_screen() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer history meta box.
|
||||
*
|
||||
* @var CustomerHistory $customer_history_meta_box
|
||||
*/
|
||||
$customer_history_meta_box = wc_get_container()->get( CustomerHistory::class );
|
||||
|
||||
add_meta_box(
|
||||
'woocommerce-customer-history',
|
||||
__( 'Customer history', 'woocommerce' ),
|
||||
function ( $post_or_order ) use ( $customer_history_meta_box ) {
|
||||
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$customer_history_meta_box->output( $order );
|
||||
}
|
||||
},
|
||||
$screen_id,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_order_update() {
|
||||
if ( ! isset( $this->order ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'edit_order' !== sanitize_text_field( wp_unslash( $_POST['action'] ?? '' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param int Order ID.
|
||||
* @param \WC_Order Post object.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
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;
|
||||
|
||||
// Claim lock.
|
||||
$edit_lock = wc_get_container()->get( EditLock::class );
|
||||
$edit_lock->lock( $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the name of order edit nonce.
|
||||
*
|
||||
* @return string Nonce action name.
|
||||
*/
|
||||
private function get_order_edit_nonce_action() {
|
||||
return 'update-order_' . $this->order->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render meta box for order specific meta.
|
||||
*/
|
||||
public function render_custom_meta_box() {
|
||||
$this->custom_meta_box->output( $this->order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render order edit page.
|
||||
*/
|
||||
public function display() {
|
||||
/**
|
||||
* This is used by the order edit page to show messages in the notice fields.
|
||||
* It should be similar to post_updated_messages filter, i.e.:
|
||||
* array(
|
||||
* {order_type} => array(
|
||||
* 1 => 'Order updated.',
|
||||
* 2 => 'Custom field updated.',
|
||||
* ...
|
||||
* ).
|
||||
*
|
||||
* The index to be displayed is computed from the $_GET['message'] variable.
|
||||
*
|
||||
* @since 7.4.0.
|
||||
*/
|
||||
$messages = apply_filters( 'woocommerce_order_updated_messages', array() );
|
||||
|
||||
$message = $this->message;
|
||||
if ( isset( $_GET['message'] ) ) {
|
||||
$message = absint( $_GET['message'] );
|
||||
}
|
||||
|
||||
if ( isset( $message ) ) {
|
||||
$message = $messages[ $this->order->get_type() ][ $message ] ?? false;
|
||||
}
|
||||
|
||||
$this->render_wrapper_start( '', $message );
|
||||
$this->render_meta_boxes();
|
||||
$this->render_wrapper_end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to render wrapper start.
|
||||
*
|
||||
* @param string $notice Notice to display, if any.
|
||||
* @param string $message Message to display, if any.
|
||||
*/
|
||||
private function render_wrapper_start( $notice = '', $message = '' ) {
|
||||
$post_type = get_post_type_object( $this->order->get_type() );
|
||||
|
||||
$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 = $this->get_page_controller()->get_new_page_url( $this->order->get_type() );
|
||||
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline">
|
||||
<?php
|
||||
echo 'new_order' === $this->current_action ? esc_html( $post_type->labels->add_new_item ) : esc_html( $post_type->labels->edit_item );
|
||||
?>
|
||||
</h1>
|
||||
<?php
|
||||
if ( 'edit_order' === $this->current_action ) {
|
||||
echo ' <a href="' . esc_url( $new_page_url ) . '" class="page-title-action">' . esc_html( $post_type->labels->add_new ) . '</a>';
|
||||
}
|
||||
?>
|
||||
<hr class="wp-header-end">
|
||||
|
||||
<?php
|
||||
if ( $notice ) :
|
||||
?>
|
||||
<div id="notice" class="notice notice-warning"><p
|
||||
id="has-newer-autosave"><?php echo wp_kses_post( $notice ); ?></p></div>
|
||||
<?php endif; ?>
|
||||
<?php if ( $message ) : ?>
|
||||
<div id="message" class="updated notice notice-success is-dismissible">
|
||||
<p><?php echo wp_kses_post( $message ); ?></p></div>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
|
||||
<form name="order" action="<?php echo esc_url( $edit_page_url ); ?>" method="post" id="order"
|
||||
<?php
|
||||
/**
|
||||
* Fires inside the order edit form tag.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*/
|
||||
do_action( 'order_edit_form_tag', $this->order );
|
||||
?>
|
||||
>
|
||||
<?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 );
|
||||
|
||||
wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
|
||||
wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
|
||||
?>
|
||||
<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'; ?>">
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to render meta boxes.
|
||||
*/
|
||||
private function render_meta_boxes() {
|
||||
?>
|
||||
<div id="postbox-container-1" class="postbox-container">
|
||||
<?php do_meta_boxes( $this->screen_id, 'side', $this->order ); ?>
|
||||
</div>
|
||||
<div id="postbox-container-2" class="postbox-container">
|
||||
<?php
|
||||
do_meta_boxes( $this->screen_id, 'normal', $this->order );
|
||||
do_meta_boxes( $this->screen_id, 'advanced', $this->order );
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to render wrapper end.
|
||||
*/
|
||||
private function render_wrapper_end() {
|
||||
?>
|
||||
</div> <!-- /post-body -->
|
||||
</div> <!-- /poststuff -->
|
||||
</form>
|
||||
</div> <!-- /wrap -->
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
unset( $response['wp-refresh-post-lock'] );
|
||||
|
||||
$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 ) {
|
||||
$lock = $this->get_lock( $order );
|
||||
$user = $lock ? get_user_by( 'id', $lock['user_id'] ) : false;
|
||||
$locked = $user && ( get_current_user_id() !== $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
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
/**
|
||||
* Meta box to edit and add custom meta values for an order.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\CustomMetaDataStore;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStoreMeta;
|
||||
use WC_Order;
|
||||
use WP_Ajax_Response;
|
||||
|
||||
/**
|
||||
* Class CustomMetaBox.
|
||||
*/
|
||||
class CustomMetaBox {
|
||||
|
||||
/**
|
||||
* Update nonce shared among different meta rows.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $update_nonce;
|
||||
|
||||
/**
|
||||
* Helper method to get formatted meta data array with proper keys. This can be directly fed to `list_meta()` method.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*
|
||||
* @return array Meta data.
|
||||
*/
|
||||
private function get_formatted_order_meta_data( \WC_Order $order ) {
|
||||
$metadata = $order->get_meta_data();
|
||||
$metadata_to_list = array();
|
||||
foreach ( $metadata as $meta ) {
|
||||
$data = $meta->get_data();
|
||||
if ( is_protected_meta( $data['key'], 'order' ) ) {
|
||||
continue;
|
||||
}
|
||||
$metadata_to_list[] = array(
|
||||
'meta_id' => $data['id'],
|
||||
'meta_key' => $data['key'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- False positive, not a meta query.
|
||||
'meta_value' => maybe_serialize( $data['value'] ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- False positive, not a meta query.
|
||||
);
|
||||
}
|
||||
return $metadata_to_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the meta box to manage custom meta.
|
||||
*
|
||||
* @param \WP_Post|\WC_Order $order_or_post Post or order object that we are rendering for.
|
||||
*/
|
||||
public function output( $order_or_post ) {
|
||||
if ( is_a( $order_or_post, \WP_Post::class ) ) {
|
||||
$order = wc_get_order( $order_or_post );
|
||||
} else {
|
||||
$order = $order_or_post;
|
||||
}
|
||||
$this->render_custom_meta_form( $this->get_formatted_order_meta_data( $order ), $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to render layout and actual HTML
|
||||
*
|
||||
* @param array $metadata_to_list List of metadata to render.
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
private function render_custom_meta_form( array $metadata_to_list, \WC_Order $order ) {
|
||||
?>
|
||||
<div id="postcustomstuff">
|
||||
<div id="ajax-response"></div>
|
||||
<?php
|
||||
list_meta( $metadata_to_list );
|
||||
$this->render_meta_form( $order );
|
||||
?>
|
||||
</div>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: 1: opening documentation tag 2: closing documentation tag. */
|
||||
esc_html( __( 'Custom fields can be used to add extra metadata to an order that you can %1$suse in your theme%2$s.', 'woocommerce' ) ),
|
||||
'<a href="' . esc_attr__( 'https://wordpress.org/support/article/custom-fields/', 'woocommerce' ) . '">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute keys to display in autofill when adding new meta key entry in custom meta box.
|
||||
* Currently, returns empty keys, will be implemented after caching is merged.
|
||||
*
|
||||
* @param mixed $deprecated Unused argument. For backwards compatibility.
|
||||
* @param \WP_Post|\WC_Order $order Order object.
|
||||
*
|
||||
* @return array Array of keys to display in autofill.
|
||||
*/
|
||||
public function order_meta_keys_autofill( $deprecated, $order ) {
|
||||
if ( ! is_a( $order, \WC_Order::class ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters values for the meta key dropdown in the Custom Fields meta box.
|
||||
*
|
||||
* Compatibility filter for `postmeta_form_keys` filter.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
|
||||
* @param \WC_Order $order The current post object.
|
||||
*/
|
||||
$keys = apply_filters( 'postmeta_form_keys', null, $order );
|
||||
if ( null === $keys || ! is_array( $keys ) ) {
|
||||
/**
|
||||
* Compatibility filter for 'postmeta_form_limit', which filters the number of custom fields to retrieve
|
||||
* for the drop-down in the Custom Fields meta box.
|
||||
*
|
||||
* @since 8.8.0
|
||||
*
|
||||
* @param int $limit Number of custom fields to retrieve. Default 30.
|
||||
*/
|
||||
$limit = apply_filters( 'postmeta_form_limit', 30 );
|
||||
$keys = wc_get_container()->get( OrdersTableDataStoreMeta::class )->get_meta_keys( $limit );
|
||||
}
|
||||
|
||||
if ( $keys ) {
|
||||
natcasesort( $keys );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reimplementation of WP core's `meta_form` function. Renders meta form box.
|
||||
*
|
||||
* @param \WC_Order $order WC_Order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_meta_form( \WC_Order $order ) : void {
|
||||
$meta_key_input_id = 'metakeyselect';
|
||||
|
||||
$keys = $this->order_meta_keys_autofill( null, $order );
|
||||
?>
|
||||
<p><strong><?php esc_html_e( 'Add New Custom Field:', 'woocommerce' ); ?></strong></p>
|
||||
<table id="newmeta">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left"><label for="<?php echo esc_attr( $meta_key_input_id ); ?>"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label></th>
|
||||
<th><label for="metavalue"><?php esc_html_e( 'Value', 'woocommerce' ); ?></label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="newmetaleft" class="left">
|
||||
<?php if ( $keys ) { ?>
|
||||
<select id="metakeyselect" name="metakeyselect">
|
||||
<option value="#NONE#"><?php esc_html_e( '— Select —', 'woocommerce' ); ?></option>
|
||||
<?php
|
||||
foreach ( $keys as $key ) {
|
||||
if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_orders' ) ) {
|
||||
continue;
|
||||
}
|
||||
echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<input class="hidden" type="text" id="metakeyinput" name="metakeyinput" value="" aria-label="<?php esc_attr_e( 'New custom field name', 'woocommerce' ); ?>" />
|
||||
<button type="button" id="newmeta-button" class="button button-small hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggleClass('hidden');jQuery('#metakeyinput, #metakeyselect').filter(':visible').trigger('focus');">
|
||||
<span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span>
|
||||
<span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span>
|
||||
<?php } else { ?>
|
||||
<input type="text" id="metakeyinput" name="metakeyinput" value="" />
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea>
|
||||
<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="submit add-custom-field">
|
||||
<?php
|
||||
submit_button(
|
||||
__( 'Add Custom Field', 'woocommerce' ),
|
||||
'',
|
||||
'addmeta',
|
||||
false,
|
||||
array(
|
||||
'id' => 'newmeta-submit',
|
||||
'data-wp-lists' => 'add:the-list:newmeta',
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to verify order edit permissions.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
*
|
||||
* @return ?WC_Order WC_Order object if the user can edit the order, die otherwise.
|
||||
*/
|
||||
private function verify_order_edit_permission_for_ajax( int $order_id ): ?WC_Order {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'edit_others_shop_orders' ) ) {
|
||||
wp_send_json_error( 'missing_capabilities' );
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
wp_send_json_error( 'invalid_order_id' );
|
||||
wp_die();
|
||||
}
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables.
|
||||
*/
|
||||
public function add_meta_ajax() {
|
||||
if ( ! check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' ) ) {
|
||||
wp_send_json_error( 'invalid_nonce' );
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$order_id = (int) $_POST['order_id'] ?? 0;
|
||||
$order = $this->verify_order_edit_permission_for_ajax( $order_id );
|
||||
|
||||
$select_meta_key = trim( sanitize_text_field( wp_unslash( $_POST['metakeyselect'] ?? '' ) ) );
|
||||
$input_meta_key = trim( sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ?? '' ) ) );
|
||||
|
||||
if ( empty( $_POST['meta'] ) && in_array( $select_meta_key, array( '', '#NONE#' ), true ) && ! $input_meta_key ) {
|
||||
wp_die( 1 );
|
||||
}
|
||||
|
||||
if ( ! empty( $_POST['meta'] ) ) { // update.
|
||||
$meta = wp_unslash( $_POST['meta'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk.
|
||||
$this->handle_update_meta( $order, $meta );
|
||||
} else { // add meta.
|
||||
$meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) );
|
||||
$meta_key = $input_meta_key ? $input_meta_key : $select_meta_key;
|
||||
$this->handle_add_meta( $order, $meta_key, $meta_value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part of WP Core's `wp_ajax_add_meta`. This is re-implemented to support updating meta for custom tables.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $meta_value Meta value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) {
|
||||
$count = 0;
|
||||
if ( is_protected_meta( $meta_key ) ) {
|
||||
wp_send_json_error( 'protected_meta' );
|
||||
wp_die();
|
||||
}
|
||||
$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',
|
||||
'id' => $meta_id,
|
||||
'data' => $this->list_meta_row(
|
||||
array(
|
||||
'meta_id' => $meta_id,
|
||||
'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
|
||||
'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
|
||||
),
|
||||
$count
|
||||
),
|
||||
'position' => 1,
|
||||
)
|
||||
);
|
||||
$response->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updating metadata.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $meta Meta object to update.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_update_meta( WC_Order $order, array $meta ) {
|
||||
if ( ! is_array( $meta ) ) {
|
||||
wp_send_json_error( 'invalid_meta' );
|
||||
wp_die();
|
||||
}
|
||||
array_walk( $meta, 'sanitize_text_field' );
|
||||
$mid = (int) key( $meta );
|
||||
if ( ! $mid ) {
|
||||
wp_send_json_error( 'invalid_meta_id' );
|
||||
wp_die();
|
||||
}
|
||||
$key = $meta[ $mid ]['key'];
|
||||
$value = $meta[ $mid ]['value'];
|
||||
if ( is_protected_meta( $key ) ) {
|
||||
wp_send_json_error( 'protected_meta' );
|
||||
wp_die();
|
||||
}
|
||||
if ( '' === trim( $key ) ) {
|
||||
wp_send_json_error( 'invalid_meta_key' );
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$order->update_meta_data( $key, $value, $mid );
|
||||
$order->save_meta_data();
|
||||
$response = new WP_Ajax_Response(
|
||||
array(
|
||||
'what' => 'meta',
|
||||
'id' => $mid,
|
||||
'old_id' => $mid,
|
||||
'data' => $this->list_meta_row(
|
||||
array(
|
||||
'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
|
||||
'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
|
||||
'meta_id' => $mid,
|
||||
),
|
||||
$count
|
||||
),
|
||||
'position' => 0,
|
||||
)
|
||||
);
|
||||
$response->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single row of public meta data in the Custom Fields meta box.
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param array $entry Meta entry.
|
||||
* @param int $count Sequence number of meta entries.
|
||||
* @return string
|
||||
*/
|
||||
private function list_meta_row( array $entry, int &$count ) : string {
|
||||
if ( is_protected_meta( $entry['meta_key'], 'post' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! $this->update_nonce ) {
|
||||
$this->update_nonce = wp_create_nonce( 'add-meta' );
|
||||
}
|
||||
|
||||
$r = '';
|
||||
++ $count;
|
||||
|
||||
if ( is_serialized( $entry['meta_value'] ) ) {
|
||||
if ( is_serialized_string( $entry['meta_value'] ) ) {
|
||||
// This is a serialized string, so we should display it.
|
||||
$entry['meta_value'] = maybe_unserialize( $entry['meta_value'] ); // // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
|
||||
} else {
|
||||
// This is a serialized array/object so we should NOT display it.
|
||||
--$count;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$entry['meta_key'] = esc_attr( $entry['meta_key'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
|
||||
$entry['meta_value'] = esc_textarea( $entry['meta_value'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
|
||||
$entry['meta_id'] = (int) $entry['meta_id'];
|
||||
|
||||
$delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] );
|
||||
|
||||
$r .= "\n\t<tr id='meta-{$entry['meta_id']}'>";
|
||||
$r .= "\n\t\t<td class='left'><label class='screen-reader-text' for='meta-{$entry['meta_id']}-key'>" . __( 'Key', 'woocommerce' ) . "</label><input name='meta[{$entry['meta_id']}][key]' id='meta-{$entry['meta_id']}-key' type='text' size='20' value='{$entry['meta_key']}' />";
|
||||
|
||||
$r .= "\n\t\t<div class='submit'>";
|
||||
$r .= get_submit_button( __( 'Delete', 'woocommerce' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce:$delete_nonce" ) );
|
||||
$r .= "\n\t\t";
|
||||
$r .= get_submit_button( __( 'Update', 'woocommerce' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta={$this->update_nonce}" ) );
|
||||
$r .= '</div>';
|
||||
$r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false );
|
||||
$r .= '</td>';
|
||||
|
||||
$r .= "\n\t\t<td><label class='screen-reader-text' for='meta-{$entry['meta_id']}-value'>" . __( 'Value', 'woocommerce' ) . "</label><textarea name='meta[{$entry['meta_id']}][value]' id='meta-{$entry['meta_id']}-value' rows='2' cols='30'>{$entry['meta_value']}</textarea></td>\n\t</tr>";
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reimplementation of WP core's `wp_ajax_delete_meta` method to support order custom meta updates with custom tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_meta_ajax() {
|
||||
$meta_id = (int) $_POST['id'] ?? 0;
|
||||
$order_id = (int) $_POST['order_id'] ?? 0;
|
||||
if ( ! $meta_id || ! $order_id ) {
|
||||
wp_send_json_error( 'invalid_meta_id' );
|
||||
wp_die();
|
||||
}
|
||||
check_ajax_referer( "delete-meta_$meta_id" );
|
||||
|
||||
$order = $this->verify_order_edit_permission_for_ajax( $order_id );
|
||||
$meta_to_delete = wp_list_filter( $order->get_meta_data(), array( 'id' => $meta_id ) );
|
||||
|
||||
if ( empty( $meta_to_delete ) ) {
|
||||
wp_send_json_error( 'invalid_meta_id' );
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$order->delete_meta_data_by_mid( $meta_id );
|
||||
if ( $order->save() ) {
|
||||
wp_die( 1 );
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Customers\Query as CustomersQuery;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class CustomerHistory
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class CustomerHistory {
|
||||
|
||||
/**
|
||||
* Output the customer history template for the order.
|
||||
*
|
||||
* @param WC_Order $order The order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ): void {
|
||||
// No history when adding a new order.
|
||||
if ( 'auto-draft' === $order->get_status() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_history = null;
|
||||
|
||||
if ( method_exists( $order, 'get_report_customer_id' ) ) {
|
||||
$customer_history = $this->get_customer_history( $order->get_report_customer_id() );
|
||||
}
|
||||
|
||||
if ( ! $customer_history ) {
|
||||
$customer_history = array(
|
||||
'orders_count' => 0,
|
||||
'total_spend' => 0,
|
||||
'avg_order_value' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
wc_get_template( 'order/customer-history.php', $customer_history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order history for the customer (data matches Customers report).
|
||||
*
|
||||
* @param int $customer_report_id The reports customer ID (not necessarily User ID).
|
||||
*
|
||||
* @return array|null Order count, total spend, and average spend per order.
|
||||
*/
|
||||
private function get_customer_history( $customer_report_id ): ?array {
|
||||
|
||||
$args = array(
|
||||
'customers' => array( $customer_report_id ),
|
||||
// If unset, these params have default values that affect the results.
|
||||
'order_after' => null,
|
||||
'order_before' => null,
|
||||
);
|
||||
|
||||
$customers_query = new CustomersQuery( $args );
|
||||
$customer_data = $customers_query->get_data();
|
||||
return $customer_data->data[0] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class OrderAttribution
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class OrderAttribution {
|
||||
|
||||
use OrderAttributionMeta;
|
||||
|
||||
/**
|
||||
* OrderAttribution constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->set_fields_and_prefix();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the meta data for display.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $meta The array of meta data to format.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function format_meta_data( array &$meta ) {
|
||||
|
||||
if ( array_key_exists( 'device_type', $meta ) ) {
|
||||
|
||||
switch ( $meta['device_type'] ) {
|
||||
case 'Mobile':
|
||||
$meta['device_type'] = __( 'Mobile', 'woocommerce' );
|
||||
break;
|
||||
case 'Tablet':
|
||||
$meta['device_type'] = __( 'Tablet', 'woocommerce' );
|
||||
break;
|
||||
case 'Desktop':
|
||||
$meta['device_type'] = __( 'Desktop', 'woocommerce' );
|
||||
break;
|
||||
|
||||
default:
|
||||
$meta['device_type'] = __( 'Unknown', 'woocommerce' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the attribution data metabox for the order.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param WC_Order $order The order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ) {
|
||||
$meta = $this->filter_meta_data( $order->get_meta_data() );
|
||||
|
||||
// If we don't have any meta to show, return.
|
||||
if ( empty( $meta ) ) {
|
||||
esc_html_e( 'No order source data available.', 'woocommerce' );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->format_meta_data( $meta );
|
||||
|
||||
// No more details if there is only the origin value - this is for unknown source types.
|
||||
$has_more_details = array( 'origin' ) !== array_keys( $meta );
|
||||
|
||||
// For direct, web admin, or mobile app orders, also don't show more details.
|
||||
$simple_sources = array( 'typein', 'admin', 'mobile_app' );
|
||||
if ( isset( $meta['source_type'] ) && in_array( $meta['source_type'], $simple_sources, true ) ) {
|
||||
$has_more_details = false;
|
||||
}
|
||||
|
||||
$template_data = array(
|
||||
'meta' => $meta,
|
||||
'has_more_details' => $has_more_details,
|
||||
);
|
||||
wc_get_template( 'order/attribution-details.php', $template_data );
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Controls the different pages/screens associated to the "Orders" menu page.
|
||||
*/
|
||||
class PageController {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* The order type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $order_type = '';
|
||||
|
||||
/**
|
||||
* Instance of the posts redirection controller.
|
||||
*
|
||||
* @var PostsRedirectionController
|
||||
*/
|
||||
private $redirection_controller;
|
||||
|
||||
/**
|
||||
* Instance of the orders list table.
|
||||
*
|
||||
* @var ListTable
|
||||
*/
|
||||
private $orders_table;
|
||||
|
||||
/**
|
||||
* Instance of orders edit form.
|
||||
*
|
||||
* @var Edit
|
||||
*/
|
||||
private $order_edit_form;
|
||||
|
||||
/**
|
||||
* Current action.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $current_action = '';
|
||||
|
||||
/**
|
||||
* Order object to be used in edit/new form.
|
||||
*
|
||||
* @var \WC_Order
|
||||
*/
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* Verify that user has permission to edit orders.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function verify_edit_permission() {
|
||||
if ( 'edit_order' === $this->current_action && ( ! isset( $this->order ) || ! $this->order ) ) {
|
||||
wp_die( esc_html__( 'You attempted to edit an order that does not exist. Perhaps it was deleted?', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( $this->order->get_type() !== $this->order_type ) {
|
||||
wp_die( esc_html__( 'Order type mismatch.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->edit_post, $this->order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( 'trash' === $this->order->get_status() ) {
|
||||
wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that user has permission to create order.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function verify_create_permission() {
|
||||
if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->publish_posts ) && ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You don\'t have permission to create a new order', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( isset( $this->order ) ) {
|
||||
$this->verify_edit_permission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
if ( 'admin_menu' === current_action() ) {
|
||||
$this->register_menu();
|
||||
} else {
|
||||
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;
|
||||
|
||||
$this->order_type = str_replace( array( 'wc-orders--', 'wc-orders' ), '', $plugin_page );
|
||||
$this->order_type = empty( $this->order_type ) ? 'shop_order' : $this->order_type;
|
||||
|
||||
$wc_order_type = wc_get_order_type( $this->order_type );
|
||||
$wp_order_type = get_post_type_object( $this->order_type );
|
||||
|
||||
if ( ! $wc_order_type || ! $wp_order_type || ! $wp_order_type->show_ui || ! current_user_can( $wp_order_type->cap->edit_posts ) ) {
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current action based on querystring arguments. Defaults to 'list_orders'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function set_action(): void {
|
||||
switch ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) {
|
||||
case 'edit':
|
||||
$this->current_action = 'edit_order';
|
||||
break;
|
||||
case 'new':
|
||||
$this->current_action = 'new_order';
|
||||
break;
|
||||
default:
|
||||
$this->current_action = 'list_orders';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the "Orders" menu.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_menu(): void {
|
||||
$order_types = wc_get_order_types( 'admin-menu' );
|
||||
|
||||
foreach ( $order_types as $order_type ) {
|
||||
$post_type = get_post_type_object( $order_type );
|
||||
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
$post_type->labels->name,
|
||||
$post_type->labels->menu_name,
|
||||
$post_type->cap->edit_posts,
|
||||
'wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ),
|
||||
array( $this, 'output' )
|
||||
);
|
||||
}
|
||||
|
||||
// In some cases (such as if the authoritative order store was changed earlier in the current request) we
|
||||
// need an extra step to remove the menu entry for the menu post type.
|
||||
add_action(
|
||||
'admin_init',
|
||||
function() use ( $order_types ) {
|
||||
foreach ( $order_types as $order_type ) {
|
||||
remove_submenu_page( 'woocommerce', 'edit.php?post_type=' . $order_type );
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs content for the current orders screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function output(): void {
|
||||
switch ( $this->current_action ) {
|
||||
case 'edit_order':
|
||||
case 'new_order':
|
||||
$this->order_edit_form->display();
|
||||
break;
|
||||
case 'list_orders':
|
||||
default:
|
||||
$this->orders_table->prepare_items();
|
||||
$this->orders_table->display();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles initialization of the orders list table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_action_list_orders(): void {
|
||||
$this->orders_table = wc_get_container()->get( ListTable::class );
|
||||
$this->orders_table->setup(
|
||||
array(
|
||||
'order_type' => $this->order_type,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $this->orders_table->current_action() ) {
|
||||
$this->orders_table->handle_bulk_actions();
|
||||
}
|
||||
|
||||
$this->strip_http_referer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a redirect to remove the `_wp_http_referer` and `_wpnonce` strings if present in the URL (see also
|
||||
* wp-admin/edit.php where a similar process takes place), otherwise the size of this field builds to an
|
||||
* unmanageable length over time.
|
||||
*/
|
||||
private function strip_http_referer(): void {
|
||||
$current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
|
||||
$stripped_url = remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), $current_url );
|
||||
|
||||
if ( $stripped_url !== $current_url ) {
|
||||
wp_safe_redirect( $stripped_url );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_action_edit_order(): void {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles initialization of the orders edit form with a new order.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_action_new_order(): void {
|
||||
global $theorder;
|
||||
|
||||
$this->verify_create_permission();
|
||||
|
||||
$order_class_name = wc_get_order_type( $this->order_type )['class_name'];
|
||||
if ( ! $order_class_name || ! class_exists( $order_class_name ) ) {
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current order type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_order_type() {
|
||||
return $this->order_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a link to the main orders screen.
|
||||
*
|
||||
* @return string Orders screen URL.
|
||||
*/
|
||||
public function get_orders_url(): string {
|
||||
return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
|
||||
admin_url( 'admin.php?page=wc-orders' ) :
|
||||
admin_url( 'edit.php?post_type=shop_order' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate edit link for an order.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
*
|
||||
* @return string Edit link.
|
||||
*/
|
||||
public function get_edit_url( int $order_id ) : string {
|
||||
if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
|
||||
return admin_url( 'post.php?post=' . absint( $order_id ) ) . '&action=edit';
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
// Confirm we could obtain the order object (since it's possible it will not exist, due to a sync issue, or may
|
||||
// have been deleted in a separate concurrent request).
|
||||
if ( false === $order ) {
|
||||
wc_get_logger()->debug(
|
||||
sprintf(
|
||||
/* translators: %d order ID. */
|
||||
__( 'Attempted to determine the edit URL for order %d, however the order does not exist.', 'woocommerce' ),
|
||||
$order_id
|
||||
)
|
||||
);
|
||||
$order_type = 'shop_order';
|
||||
} else {
|
||||
$order_type = $order->get_type();
|
||||
}
|
||||
|
||||
return add_query_arg(
|
||||
array(
|
||||
'action' => 'edit',
|
||||
'id' => absint( $order_id ),
|
||||
),
|
||||
$this->get_base_page_url( $order_type )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a link for creating order.
|
||||
*
|
||||
* @param string $order_type The order type. Defaults to 'shop_order'.
|
||||
* @return string
|
||||
*/
|
||||
public function get_new_page_url( $order_type = 'shop_order' ) : string {
|
||||
$url = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
|
||||
add_query_arg( 'action', 'new', $this->get_base_page_url( $order_type ) ) :
|
||||
admin_url( 'post-new.php?post_type=' . $order_type );
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a link to the main screen for a custom order type.
|
||||
*
|
||||
* @param string $order_type The order type.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \Exception When an invalid order type is passed.
|
||||
*/
|
||||
public function get_base_page_url( $order_type ): string {
|
||||
$order_types_with_ui = wc_get_order_types( 'admin-menu' );
|
||||
|
||||
if ( ! in_array( $order_type, $order_types_with_ui, true ) ) {
|
||||
// translators: %s is a custom order type.
|
||||
throw new \Exception( sprintf( __( 'Invalid order type: %s.', 'woocommerce' ), esc_html( $order_type ) ) );
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
|
||||
/**
|
||||
* When {@see OrdersTableDataStore} is in use, this class takes care of redirecting admins from CPT-based URLs
|
||||
* to the new ones.
|
||||
*/
|
||||
class PostsRedirectionController {
|
||||
|
||||
/**
|
||||
* Instance of the PageController class.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $page_controller;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param PageController $page_controller Page controller instance. Used to generate links/URLs.
|
||||
*/
|
||||
public function __construct( PageController $page_controller ) {
|
||||
$this->page_controller = $page_controller;
|
||||
|
||||
if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'load-edit.php',
|
||||
function() {
|
||||
$this->maybe_redirect_to_orders_page();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'load-post-new.php',
|
||||
function() {
|
||||
$this->maybe_redirect_to_new_order_page();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'load-post.php',
|
||||
function() {
|
||||
$this->maybe_redirect_to_edit_order_page();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, performs a redirection to the main orders page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_redirect_to_orders_page(): void {
|
||||
$post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
|
||||
if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect query args, except for 'post_type'.
|
||||
$query_args = wp_unslash( $_GET );
|
||||
$action = $query_args['action'] ?? '';
|
||||
$posts = $query_args['post'] ?? array();
|
||||
unset( $query_args['post_type'], $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );
|
||||
|
||||
// Remap 'post_status' arg.
|
||||
if ( isset( $query_args['post_status'] ) ) {
|
||||
$query_args['status'] = $query_args['post_status'];
|
||||
unset( $query_args['post_status'] );
|
||||
}
|
||||
|
||||
$new_url = $this->page_controller->get_base_page_url( $post_type );
|
||||
$new_url = add_query_arg( $query_args, $new_url );
|
||||
|
||||
// Handle bulk actions.
|
||||
if ( $action && in_array( $action, array( 'trash', 'untrash', 'delete', 'mark_processing', 'mark_on-hold', 'mark_completed', 'mark_cancelled' ), true ) ) {
|
||||
check_admin_referer( 'bulk-posts' );
|
||||
|
||||
$new_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'id' => $posts,
|
||||
'_wp_http_referer' => $this->page_controller->get_orders_url(),
|
||||
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
|
||||
),
|
||||
$new_url
|
||||
);
|
||||
}
|
||||
|
||||
wp_safe_redirect( $new_url, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, performs a redirection to the new order page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_redirect_to_new_order_page(): void {
|
||||
$post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
|
||||
if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect query args, except for 'post_type'.
|
||||
$query_args = wp_unslash( $_GET );
|
||||
unset( $query_args['post_type'] );
|
||||
|
||||
$new_url = $this->page_controller->get_new_page_url( $post_type );
|
||||
$new_url = add_query_arg( $query_args, $new_url );
|
||||
|
||||
wp_safe_redirect( $new_url, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, performs a redirection to the edit order page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_redirect_to_edit_order_page(): void {
|
||||
$post_id = absint( $_GET['post'] ?? 0 );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$redirect_from_types = wc_get_order_types( 'admin-menu' );
|
||||
$redirect_from_types[] = 'shop_order_placehold';
|
||||
|
||||
$post_type = get_post_type( $post_id );
|
||||
$order_type = $post_type ? $post_type : OrderUtil::get_order_type( $post_id );
|
||||
if ( ! in_array( $order_type, $redirect_from_types, true ) || ! isset( $_GET['action'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect query args, except for 'post'.
|
||||
$query_args = wp_unslash( $_GET );
|
||||
$action = $query_args['action'];
|
||||
unset( $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );
|
||||
|
||||
$new_url = '';
|
||||
|
||||
switch ( $action ) {
|
||||
case 'edit':
|
||||
$new_url = $this->page_controller->get_edit_url( $post_id );
|
||||
break;
|
||||
|
||||
case 'trash':
|
||||
case 'untrash':
|
||||
case 'delete':
|
||||
// Re-generate nonce if validation passes.
|
||||
check_admin_referer( $action . '-post_' . $post_id );
|
||||
|
||||
$new_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'order' => array( $post_id ),
|
||||
'_wp_http_referer' => $this->page_controller->get_orders_url(),
|
||||
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
|
||||
),
|
||||
$this->page_controller->get_orders_url()
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $new_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$new_url = add_query_arg( $query_args, $new_url );
|
||||
|
||||
wp_safe_redirect( $new_url, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract class for product form components.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
/**
|
||||
* Component class.
|
||||
*/
|
||||
abstract class Component {
|
||||
/**
|
||||
* Product Component traits.
|
||||
*/
|
||||
use ComponentTrait;
|
||||
|
||||
/**
|
||||
* Component additional arguments.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $additional_args;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $id Component id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $additional_args Array containing additional arguments.
|
||||
*/
|
||||
public function __construct( $id, $plugin_id, $additional_args ) {
|
||||
$this->id = $id;
|
||||
$this->plugin_id = $plugin_id;
|
||||
$this->additional_args = $additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component arguments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_additional_args() {
|
||||
return $this->additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component arguments.
|
||||
*
|
||||
* @param string $key key of argument.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_additional_argument( $key ) {
|
||||
return self::get_argument_from_path( $this->additional_args, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component as JSON.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_json() {
|
||||
return array_merge(
|
||||
array(
|
||||
'id' => $this->get_id(),
|
||||
'plugin_id' => $this->get_plugin_id(),
|
||||
),
|
||||
$this->get_additional_args()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting function for product form component.
|
||||
*
|
||||
* @param Component $a Component a.
|
||||
* @param Component $b Component b.
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return int
|
||||
*/
|
||||
public static function sort( $a, $b, $sort_by = array() ) {
|
||||
$key = $sort_by['key'];
|
||||
$a_val = $a->get_additional_argument( $key );
|
||||
$b_val = $b->get_additional_argument( $key );
|
||||
if ( 'asc' === $sort_by['order'] ) {
|
||||
return $a_val <=> $b_val;
|
||||
} else {
|
||||
return $b_val <=> $a_val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets argument by dot notation path.
|
||||
*
|
||||
* @param array $arguments Arguments array.
|
||||
* @param string $path Path for argument key.
|
||||
* @param string $delimiter Path delimiter, default: '.'.
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function get_argument_from_path( $arguments, $path, $delimiter = '.' ) {
|
||||
$path_keys = explode( $delimiter, $path );
|
||||
$num_keys = false !== $path_keys ? count( $path_keys ) : 0;
|
||||
|
||||
$val = $arguments;
|
||||
for ( $i = 0; $i < $num_keys; $i++ ) {
|
||||
$key = $path_keys[ $i ];
|
||||
if ( array_key_exists( $key, $val ) ) {
|
||||
$val = $val[ $key ];
|
||||
} else {
|
||||
$val = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of required arguments.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $required_arguments = array();
|
||||
|
||||
/**
|
||||
* Get missing arguments of args array.
|
||||
*
|
||||
* @param array $args field arguments.
|
||||
* @return array
|
||||
*/
|
||||
public function get_missing_arguments( $args ) {
|
||||
return array_values(
|
||||
array_filter(
|
||||
$this->required_arguments,
|
||||
function( $arg_key ) use ( $args ) {
|
||||
return null === self::get_argument_from_path( $args, $arg_key );
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Form Traits
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* ComponentTrait class.
|
||||
*/
|
||||
trait ComponentTrait {
|
||||
/**
|
||||
* Component ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $plugin_id;
|
||||
|
||||
/**
|
||||
* Product form component location.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $location;
|
||||
|
||||
/**
|
||||
* Product form component order.
|
||||
*
|
||||
* @var number
|
||||
*/
|
||||
protected $order;
|
||||
|
||||
/**
|
||||
* Return id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return plugin id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_plugin_id() {
|
||||
return $this->plugin_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles product form field related methods.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
/**
|
||||
* Field class.
|
||||
*/
|
||||
class Field extends Component {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $id Field id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $additional_args Array containing the necessary arguments.
|
||||
* $args = array(
|
||||
* 'type' => (string) Field type. Required.
|
||||
* 'section' => (string) Field location. Required.
|
||||
* 'order' => (int) Field order.
|
||||
* 'properties' => (array) Field properties.
|
||||
* ).
|
||||
* @throws \Exception If there are missing arguments.
|
||||
*/
|
||||
public function __construct( $id, $plugin_id, $additional_args ) {
|
||||
parent::__construct( $id, $plugin_id, $additional_args );
|
||||
$this->required_arguments = array(
|
||||
'type',
|
||||
'section',
|
||||
'properties.name',
|
||||
'properties.label',
|
||||
);
|
||||
|
||||
$missing_arguments = self::get_missing_arguments( $additional_args );
|
||||
if ( count( $missing_arguments ) > 0 ) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
/* translators: 1: Missing arguments list. */
|
||||
esc_html__( 'You are missing required arguments of WooCommerce ProductForm Field: %1$s', 'woocommerce' ),
|
||||
join( ', ', $missing_arguments )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Form Factory
|
||||
*
|
||||
* @package Woocommerce ProductForm
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Factory that contains logic for the WooCommerce Product Form.
|
||||
*/
|
||||
class FormFactory {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Form instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Store form fields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $form_fields = array();
|
||||
|
||||
/**
|
||||
* Store form cards.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $form_subsections = array();
|
||||
|
||||
/**
|
||||
* Store form sections.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $form_sections = array();
|
||||
|
||||
/**
|
||||
* Store form tabs.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $form_tabs = array();
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
final public static function instance() {
|
||||
if ( ! static::$instance ) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() { }
|
||||
|
||||
/**
|
||||
* Adds a field to the product form.
|
||||
*
|
||||
* @param string $id Field id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $args Array containing the necessary arguments.
|
||||
* $args = array(
|
||||
* 'type' => (string) Field type. Required.
|
||||
* 'section' => (string) Field location. Required.
|
||||
* 'order' => (int) Field order.
|
||||
* 'properties' => (array) Field properties.
|
||||
* 'name' => (string) Field name.
|
||||
* ).
|
||||
* @return Field|WP_Error New field or WP_Error.
|
||||
*/
|
||||
public static function add_field( $id, $plugin_id, $args ) {
|
||||
$new_field = self::create_item( 'field', 'Field', $id, $plugin_id, $args );
|
||||
if ( is_wp_error( $new_field ) ) {
|
||||
return $new_field;
|
||||
}
|
||||
self::$form_fields[ $id ] = $new_field;
|
||||
return $new_field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Subsection to the product form.
|
||||
*
|
||||
* @param string $id Subsection id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $args Array containing the necessary arguments.
|
||||
* @return Subsection|WP_Error New subsection or WP_Error.
|
||||
*/
|
||||
public static function add_subsection( $id, $plugin_id, $args = array() ) {
|
||||
$new_subsection = self::create_item( 'subsection', 'Subsection', $id, $plugin_id, $args );
|
||||
if ( is_wp_error( $new_subsection ) ) {
|
||||
return $new_subsection;
|
||||
}
|
||||
self::$form_subsections[ $id ] = $new_subsection;
|
||||
return $new_subsection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a section to the product form.
|
||||
*
|
||||
* @param string $id Card id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $args Array containing the necessary arguments.
|
||||
* @return Section|WP_Error New section or WP_Error.
|
||||
*/
|
||||
public static function add_section( $id, $plugin_id, $args ) {
|
||||
$new_section = self::create_item( 'section', 'Section', $id, $plugin_id, $args );
|
||||
if ( is_wp_error( $new_section ) ) {
|
||||
return $new_section;
|
||||
}
|
||||
self::$form_sections[ $id ] = $new_section;
|
||||
return $new_section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tab to the product form.
|
||||
*
|
||||
* @param string $id Card id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $args Array containing the necessary arguments.
|
||||
* @return Tab|WP_Error New section or WP_Error.
|
||||
*/
|
||||
public static function add_tab( $id, $plugin_id, $args ) {
|
||||
$new_tab = self::create_item( 'tab', 'Tab', $id, $plugin_id, $args );
|
||||
if ( is_wp_error( $new_tab ) ) {
|
||||
return $new_tab;
|
||||
}
|
||||
self::$form_tabs[ $id ] = $new_tab;
|
||||
return $new_tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered fields.
|
||||
*
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return array list of registered fields.
|
||||
*/
|
||||
public static function get_fields( $sort_by = array(
|
||||
'key' => 'order',
|
||||
'order' => 'asc',
|
||||
) ) {
|
||||
return self::get_items( 'field', 'Field', $sort_by );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered cards.
|
||||
*
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return array list of registered cards.
|
||||
*/
|
||||
public static function get_subsections( $sort_by = array(
|
||||
'key' => 'order',
|
||||
'order' => 'asc',
|
||||
) ) {
|
||||
return self::get_items( 'subsection', 'Subsection', $sort_by );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered sections.
|
||||
*
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return array list of registered sections.
|
||||
*/
|
||||
public static function get_sections( $sort_by = array(
|
||||
'key' => 'order',
|
||||
'order' => 'asc',
|
||||
) ) {
|
||||
return self::get_items( 'section', 'Section', $sort_by );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered tabs.
|
||||
*
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return array list of registered tabs.
|
||||
*/
|
||||
public static function get_tabs( $sort_by = array(
|
||||
'key' => 'order',
|
||||
'order' => 'asc',
|
||||
) ) {
|
||||
return self::get_items( 'tab', 'Tab', $sort_by );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered items.
|
||||
*
|
||||
* @param string $type Form component type.
|
||||
* @return array List of registered items.
|
||||
*/
|
||||
private static function get_item_list( $type ) {
|
||||
$mapping = array(
|
||||
'field' => self::$form_fields,
|
||||
'subsection' => self::$form_subsections,
|
||||
'section' => self::$form_sections,
|
||||
'tab' => self::$form_tabs,
|
||||
);
|
||||
if ( array_key_exists( $type, $mapping ) ) {
|
||||
return $mapping[ $type ];
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of registered items.
|
||||
*
|
||||
* @param string $type Form component type.
|
||||
* @param class-string $class_name Class of component type.
|
||||
* @param array $sort_by key and order to sort by.
|
||||
* @return array list of registered items.
|
||||
*/
|
||||
private static function get_items( $type, $class_name, $sort_by = array(
|
||||
'key' => 'order',
|
||||
'order' => 'asc',
|
||||
) ) {
|
||||
$item_list = self::get_item_list( $type );
|
||||
$class = 'Automattic\\WooCommerce\\Internal\\Admin\\ProductForm\\' . $class_name;
|
||||
$items = array_values( $item_list );
|
||||
if ( class_exists( $class ) && method_exists( $class, 'sort' ) ) {
|
||||
usort(
|
||||
$items,
|
||||
function ( $a, $b ) use ( $sort_by, $class ) {
|
||||
return $class::sort( $a, $b, $sort_by );
|
||||
}
|
||||
);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
*
|
||||
* @param string $type Form component type.
|
||||
* @param class-string $class_name Class of component type.
|
||||
* @param string $id Item id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $args additional arguments for item.
|
||||
* @return Field|Card|Section|Tab|WP_Error New product form item or WP_Error.
|
||||
*/
|
||||
private static function create_item( $type, $class_name, $id, $plugin_id, $args ) {
|
||||
$item_list = self::get_item_list( $type );
|
||||
$class = 'Automattic\\WooCommerce\\Internal\\Admin\\ProductForm\\' . $class_name;
|
||||
if ( ! class_exists( $class ) ) {
|
||||
return new WP_Error(
|
||||
'wc_product_form_' . $type . '_missing_form_class',
|
||||
sprintf(
|
||||
/* translators: 1: missing class name. */
|
||||
esc_html__( '%1$s class does not exist.', 'woocommerce' ),
|
||||
$class
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( isset( $item_list[ $id ] ) ) {
|
||||
return new WP_Error(
|
||||
'wc_product_form_' . $type . '_duplicate_field_id',
|
||||
sprintf(
|
||||
/* translators: 1: Item type 2: Duplicate registered item id. */
|
||||
esc_html__( 'You have attempted to register a duplicate form %1$s with WooCommerce Form: %2$s', 'woocommerce' ),
|
||||
$type,
|
||||
'`' . $id . '`'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$defaults = array(
|
||||
'order' => 20,
|
||||
);
|
||||
|
||||
$item_arguments = wp_parse_args( $args, $defaults );
|
||||
|
||||
try {
|
||||
return new $class( $id, $plugin_id, $item_arguments );
|
||||
} catch ( \Exception $e ) {
|
||||
return new WP_Error(
|
||||
'wc_product_form_' . $type . '_class_creation',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles product form section related methods.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
/**
|
||||
* Section class.
|
||||
*/
|
||||
class Section extends Component {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $id Section id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $additional_args Array containing additional arguments.
|
||||
* $args = array(
|
||||
* 'order' => (int) Section order.
|
||||
* 'title' => (string) Section description.
|
||||
* 'description' => (string) Section description.
|
||||
* ).
|
||||
* @throws \Exception If there are missing arguments.
|
||||
*/
|
||||
public function __construct( $id, $plugin_id, $additional_args ) {
|
||||
parent::__construct( $id, $plugin_id, $additional_args );
|
||||
$this->required_arguments = array(
|
||||
'title',
|
||||
);
|
||||
$missing_arguments = self::get_missing_arguments( $additional_args );
|
||||
if ( count( $missing_arguments ) > 0 ) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
/* translators: 1: Missing arguments list. */
|
||||
esc_html__( 'You are missing required arguments of WooCommerce ProductForm Section: %1$s', 'woocommerce' ),
|
||||
join( ', ', $missing_arguments )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles product form SubSection related methods.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
/**
|
||||
* SubSection class.
|
||||
*/
|
||||
class Subsection extends Component {}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles product form tab related methods.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductForm;
|
||||
|
||||
/**
|
||||
* Field class.
|
||||
*/
|
||||
class Tab extends Component {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $id Field id.
|
||||
* @param string $plugin_id Plugin id.
|
||||
* @param array $additional_args Array containing the necessary arguments.
|
||||
* $args = array(
|
||||
* 'name' => (string) Tab name. Required.
|
||||
* 'title' => (string) Tab title. Required.
|
||||
* 'order' => (int) Tab order.
|
||||
* 'properties' => (array) Tab properties.
|
||||
* ).
|
||||
* @throws \Exception If there are missing arguments.
|
||||
*/
|
||||
public function __construct( $id, $plugin_id, $additional_args ) {
|
||||
parent::__construct( $id, $plugin_id, $additional_args );
|
||||
|
||||
$this->required_arguments = array(
|
||||
'name',
|
||||
'title',
|
||||
);
|
||||
|
||||
$missing_arguments = self::get_missing_arguments( $additional_args );
|
||||
if ( count( $missing_arguments ) > 0 ) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
/* translators: 1: Missing arguments list. */
|
||||
esc_html__( 'You are missing required arguments of WooCommerce ProductForm Tab: %1$s', 'woocommerce' ),
|
||||
join( ', ', $missing_arguments )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
/**
|
||||
* Products > Reviews
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WP_Ajax_Response;
|
||||
use WP_Comment;
|
||||
use WP_Screen;
|
||||
|
||||
/**
|
||||
* Handles backend logic for the Reviews component.
|
||||
*/
|
||||
class Reviews {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Admin page identifier.
|
||||
*/
|
||||
const MENU_SLUG = 'product-reviews';
|
||||
|
||||
/**
|
||||
* Reviews page hook name.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $reviews_page_hook = null;
|
||||
|
||||
/**
|
||||
* Reviews list table instance.
|
||||
*
|
||||
* @var ReviewsListTable|null
|
||||
*/
|
||||
protected $reviews_list_table;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
self::add_action( 'admin_menu', [ $this, 'add_reviews_page' ] );
|
||||
self::add_action( 'admin_enqueue_scripts', [ $this, 'load_javascript' ] );
|
||||
|
||||
// These ajax callbacks need a low priority to ensure they run before their WordPress core counterparts.
|
||||
self::add_action( 'wp_ajax_edit-comment', [ $this, 'handle_edit_review' ], -1 );
|
||||
self::add_action( 'wp_ajax_replyto-comment', [ $this, 'handle_reply_to_review' ], -1 );
|
||||
|
||||
self::add_filter( 'parent_file', [ $this, 'edit_review_parent_file' ] );
|
||||
self::add_filter( 'gettext', [ $this, 'edit_comments_screen_text' ], 10, 2 );
|
||||
self::add_action( 'admin_notices', [ $this, 'display_notices' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the required capability to access the reviews page and manage product reviews.
|
||||
*
|
||||
* @param string $context The context for which the capability is needed (e.g. `view` or `moderate`).
|
||||
* @return string
|
||||
*/
|
||||
public static function get_capability( string $context = 'view' ) : string {
|
||||
|
||||
/**
|
||||
* Filters whether the current user can manage product reviews.
|
||||
*
|
||||
* This is aligned to {@see \wc_rest_check_product_reviews_permissions()}
|
||||
*
|
||||
* @since 6.7.0
|
||||
*
|
||||
* @param string $capability The capability (defaults to `moderate_comments` for viewing and `edit_products` for editing).
|
||||
* @param string $context The context for which the capability is needed.
|
||||
*/
|
||||
return (string) apply_filters( 'woocommerce_product_reviews_page_capability', $context === 'view' ? 'moderate_comments' : 'edit_products', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the Product Reviews submenu page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_reviews_page() : void {
|
||||
|
||||
$this->reviews_page_hook = add_submenu_page(
|
||||
'edit.php?post_type=product',
|
||||
__( 'Reviews', 'woocommerce' ),
|
||||
__( 'Reviews', 'woocommerce' ) . $this->get_pending_count_bubble(),
|
||||
static::get_capability(),
|
||||
static::MENU_SLUG,
|
||||
[ $this, 'render_reviews_list_table' ]
|
||||
);
|
||||
|
||||
self::add_action( "load-{$this->reviews_page_hook}", array( $this, 'load_reviews_screen' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URL to the product reviews page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_reviews_page_url() : string {
|
||||
return add_query_arg(
|
||||
[
|
||||
'post_type' => 'product',
|
||||
'page' => static::MENU_SLUG,
|
||||
],
|
||||
admin_url( 'edit.php' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current page is the reviews page.
|
||||
*
|
||||
* @global WP_Screen $current_screen
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_reviews_page() : bool {
|
||||
global $current_screen;
|
||||
|
||||
return isset( $current_screen->base ) && $current_screen->base === 'product_page_' . static::MENU_SLUG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the JavaScript required for inline replies and quick edit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load_javascript() : void {
|
||||
if ( $this->is_reviews_page() ) {
|
||||
wp_enqueue_script( 'admin-comments' );
|
||||
enqueue_comment_hotkeys_js();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the object is a review or a reply to a review.
|
||||
*
|
||||
* @param WP_Comment|mixed $object Object to check.
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_review_or_reply( $object ) : bool {
|
||||
|
||||
$is_review_or_reply = $object instanceof WP_Comment && in_array( $object->comment_type, [ 'review', 'comment' ], true ) && get_post_type( $object->comment_post_ID ) === 'product';
|
||||
|
||||
/**
|
||||
* Filters whether the object is a review or a reply to a review.
|
||||
*
|
||||
* @since 6.7.0
|
||||
*
|
||||
* @param bool $is_review_or_reply Whether the object in context is a review or a reply to a review.
|
||||
* @param WP_Comment|mixed $object The object in context.
|
||||
*/
|
||||
return (bool) apply_filters( 'woocommerce_product_reviews_is_product_review_or_reply', $is_review_or_reply, $object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax callback for editing a review.
|
||||
*
|
||||
* This functionality is taken from {@see wp_ajax_edit_comment()} and is largely copy and pasted. The only thing
|
||||
* we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need
|
||||
* to use our own {@see ReviewsListTable} class to support our custom columns.
|
||||
*
|
||||
* This ajax callback is registered with a lower priority than WordPress core's so that our code can run
|
||||
* first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method
|
||||
* to allow the WordPress core callback to take over.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_edit_review(): void {
|
||||
// Don't interfere with comment functionality relating to the reviews meta box within the product editor.
|
||||
if ( sanitize_text_field( wp_unslash( $_POST['mode'] ?? '' ) ) === 'single' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );
|
||||
|
||||
$comment_id = isset( $_POST['comment_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_ID'] ) ) : 0;
|
||||
|
||||
if ( empty( $comment_id ) || ! current_user_can( 'edit_comment', $comment_id ) ) {
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
$review = get_comment( $comment_id );
|
||||
|
||||
// Bail silently if this is not a review, or a reply to a review. That allows `wp_ajax_edit_comment()` to handle any further actions.
|
||||
if ( ! $this->is_review_or_reply( $review ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $review->comment_ID ) ) {
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
if ( empty( $_POST['content'] ) ) {
|
||||
wp_die( esc_html__( 'Error: Please type your review text.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( isset( $_POST['status'] ) ) {
|
||||
$_POST['comment_status'] = sanitize_text_field( wp_unslash( $_POST['status'] ) );
|
||||
}
|
||||
|
||||
$updated = edit_comment();
|
||||
if ( is_wp_error( $updated ) ) {
|
||||
wp_die( esc_html( $updated->get_error_message() ) );
|
||||
}
|
||||
|
||||
$position = isset( $_POST['position'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['position'] ) ) : -1;
|
||||
$wp_list_table = $this->make_reviews_list_table();
|
||||
|
||||
ob_start();
|
||||
$wp_list_table->single_row( $review );
|
||||
$review_list_item = ob_get_clean();
|
||||
|
||||
$x = new WP_Ajax_Response();
|
||||
|
||||
$x->add(
|
||||
array(
|
||||
'what' => 'edit_comment',
|
||||
'id' => $review->comment_ID,
|
||||
'data' => $review_list_item,
|
||||
'position' => $position,
|
||||
)
|
||||
);
|
||||
|
||||
$x->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax callback for replying to a review inline.
|
||||
*
|
||||
* This functionality is taken from {@see wp_ajax_replyto_comment()} and is largely copy and pasted. The only thing
|
||||
* we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need
|
||||
* to use our own {@see ReviewsListTable} class to support our custom columns.
|
||||
*
|
||||
* This ajax callback is registered with a lower priority than WordPress core's so that our code can run
|
||||
* first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method
|
||||
* to allow the WordPress core callback to take over.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_reply_to_review() : void {
|
||||
// Don't interfere with comment functionality relating to the reviews meta box within the product editor.
|
||||
if ( sanitize_text_field( wp_unslash( $_POST['mode'] ?? '' ) ) === 'single' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );
|
||||
|
||||
$comment_post_ID = isset( $_POST['comment_post_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_post_ID'] ) ) : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
$post = get_post( $comment_post_ID ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
|
||||
if ( ! $post ) {
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
// Inline Review replies will use the `detail` mode. If that's not what we have, then let WordPress core take over.
|
||||
if ( isset( $_REQUEST['mode'] ) && $_REQUEST['mode'] === 'dashboard' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is not a a reply to a review, bail silently to let WordPress core take over.
|
||||
if ( get_post_type( $post ) !== 'product' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $comment_post_ID ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
if ( empty( $post->post_status ) ) {
|
||||
wp_die( 1 );
|
||||
} elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) {
|
||||
wp_die( esc_html__( 'Error: You can\'t reply to a review on a draft product.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
|
||||
if ( $user->exists() ) {
|
||||
$user_ID = $user->ID;
|
||||
$comment_author = wp_slash( $user->display_name );
|
||||
$comment_author_email = wp_slash( $user->user_email );
|
||||
$comment_author_url = wp_slash( $user->user_url );
|
||||
// WordPress core already sanitizes `content` during the `pre_comment_content` hook, which is why it's not needed here, {@see wp_filter_comment()} and {@see kses_init_filters()}.
|
||||
$comment_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$comment_type = isset( $_POST['comment_type'] ) ? sanitize_text_field( wp_unslash( $_POST['comment_type'] ) ) : 'comment';
|
||||
|
||||
if ( current_user_can( 'unfiltered_html' ) ) {
|
||||
if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
|
||||
$_POST['_wp_unfiltered_html_comment'] = '';
|
||||
}
|
||||
|
||||
if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) {
|
||||
kses_remove_filters(); // Start with a clean slate.
|
||||
kses_init_filters(); // Set up the filters.
|
||||
remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
|
||||
add_filter( 'pre_comment_content', 'wp_filter_kses' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wp_die( esc_html__( 'Sorry, you must be logged in to reply to a review.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( $comment_content === '' ) {
|
||||
wp_die( esc_html__( 'Error: Please type your reply text.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$comment_parent = 0;
|
||||
|
||||
if ( isset( $_POST['comment_ID'] ) ) {
|
||||
$comment_parent = absint( wp_unslash( $_POST['comment_ID'] ) );
|
||||
}
|
||||
|
||||
$comment_auto_approved = false;
|
||||
$commentdata = compact( 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID' );
|
||||
|
||||
// Automatically approve parent comment.
|
||||
if ( ! empty( $_POST['approve_parent'] ) ) {
|
||||
$parent = get_comment( $comment_parent );
|
||||
|
||||
if ( $parent && $parent->comment_approved === '0' && $parent->comment_post_ID === $comment_post_ID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) {
|
||||
wp_die( -1 );
|
||||
}
|
||||
|
||||
if ( wp_set_comment_status( $parent, 'approve' ) ) {
|
||||
$comment_auto_approved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$comment_id = wp_new_comment( $commentdata );
|
||||
|
||||
if ( is_wp_error( $comment_id ) ) {
|
||||
wp_die( esc_html( $comment_id->get_error_message() ) );
|
||||
}
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
wp_die( 1 );
|
||||
}
|
||||
|
||||
$position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
|
||||
|
||||
ob_start();
|
||||
$wp_list_table = $this->make_reviews_list_table();
|
||||
$wp_list_table->single_row( $comment );
|
||||
$comment_list_item = ob_get_clean();
|
||||
|
||||
$response = array(
|
||||
'what' => 'comment',
|
||||
'id' => $comment->comment_ID,
|
||||
'data' => $comment_list_item,
|
||||
'position' => $position,
|
||||
);
|
||||
|
||||
$counts = wp_count_comments();
|
||||
$response['supplemental'] = array(
|
||||
'in_moderation' => $counts->moderated,
|
||||
'i18n_comments_text' => sprintf(
|
||||
/* translators: %s: Number of reviews. */
|
||||
_n( '%s Review', '%s Reviews', $counts->approved, 'woocommerce' ),
|
||||
number_format_i18n( $counts->approved )
|
||||
),
|
||||
'i18n_moderation_text' => sprintf(
|
||||
/* translators: %s: Number of reviews. */
|
||||
_n( '%s Review in moderation', '%s Reviews in moderation', $counts->moderated, 'woocommerce' ),
|
||||
number_format_i18n( $counts->moderated )
|
||||
),
|
||||
);
|
||||
|
||||
if ( $comment_auto_approved && isset( $parent ) ) {
|
||||
$response['supplemental']['parent_approved'] = $parent->comment_ID;
|
||||
$response['supplemental']['parent_post_id'] = $parent->comment_post_ID;
|
||||
}
|
||||
|
||||
$x = new WP_Ajax_Response();
|
||||
$x->add( $response );
|
||||
$x->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays notices on the Reviews page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function display_notices() : void {
|
||||
|
||||
if ( $this->is_reviews_page() ) {
|
||||
$this->maybe_display_reviews_bulk_action_notice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* May display the bulk action admin notice.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function maybe_display_reviews_bulk_action_notice() : void {
|
||||
|
||||
$messages = $this->get_bulk_action_notice_messages();
|
||||
|
||||
echo ! empty( $messages ) ? '<div id="moderated" class="updated"><p>' . implode( "<br/>\n", $messages ) . '</p></div>' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the applicable bulk action admin notice messages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_bulk_action_notice_messages() : array {
|
||||
|
||||
$approved = isset( $_REQUEST['approved'] ) ? (int) $_REQUEST['approved'] : 0;
|
||||
$unapproved = isset( $_REQUEST['unapproved'] ) ? (int) $_REQUEST['unapproved'] : 0;
|
||||
$deleted = isset( $_REQUEST['deleted'] ) ? (int) $_REQUEST['deleted'] : 0;
|
||||
$trashed = isset( $_REQUEST['trashed'] ) ? (int) $_REQUEST['trashed'] : 0;
|
||||
$untrashed = isset( $_REQUEST['untrashed'] ) ? (int) $_REQUEST['untrashed'] : 0;
|
||||
$spammed = isset( $_REQUEST['spammed'] ) ? (int) $_REQUEST['spammed'] : 0;
|
||||
$unspammed = isset( $_REQUEST['unspammed'] ) ? (int) $_REQUEST['unspammed'] : 0;
|
||||
|
||||
$messages = [];
|
||||
|
||||
if ( $approved > 0 ) {
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review approved', '%s reviews approved', $approved, 'woocommerce' ), $approved );
|
||||
}
|
||||
|
||||
if ( $unapproved > 0 ) {
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review unapproved', '%s reviews unapproved', $unapproved, 'woocommerce' ), $unapproved );
|
||||
}
|
||||
|
||||
if ( $spammed > 0 ) {
|
||||
$ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0;
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review marked as spam.', '%s reviews marked as spam.', $spammed, 'woocommerce' ), $spammed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=unspam&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />';
|
||||
}
|
||||
|
||||
if ( $unspammed > 0 ) {
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review restored from the spam', '%s reviews restored from the spam', $unspammed, 'woocommerce' ), $unspammed );
|
||||
}
|
||||
|
||||
if ( $trashed > 0 ) {
|
||||
$ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0;
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review moved to the Trash.', '%s reviews moved to the Trash.', $trashed, 'woocommerce' ), $trashed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=untrash&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />';
|
||||
}
|
||||
|
||||
if ( $untrashed > 0 ) {
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review restored from the Trash', '%s reviews restored from the Trash', $untrashed, 'woocommerce' ), $untrashed );
|
||||
}
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
/* translators: %s is an integer higher than 0 (1, 2, 3...) */
|
||||
$messages[] = sprintf( _n( '%s review permanently deleted', '%s reviews permanently deleted', $deleted, 'woocommerce' ), $deleted );
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of pending product reviews/replies, and returns the notification bubble if there's more than zero.
|
||||
*
|
||||
* @return string Empty string if there are no pending reviews, or bubble HTML if there are.
|
||||
*/
|
||||
protected function get_pending_count_bubble() : string {
|
||||
$count = (int) get_comments(
|
||||
[
|
||||
'type__in' => [ 'review', 'comment' ],
|
||||
'status' => '0',
|
||||
'post_type' => 'product',
|
||||
'count' => true,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides an opportunity to alter the pending comment count used within
|
||||
* the product reviews admin list table.
|
||||
*
|
||||
* @since 7.0.0
|
||||
*
|
||||
* @param array $count Current count of comments pending review.
|
||||
*/
|
||||
$count = apply_filters( 'woocommerce_product_reviews_pending_count', $count );
|
||||
|
||||
if ( empty( $count ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' <span class="awaiting-mod count-' . esc_attr( $count ) . '"><span class="pending-count">' . esc_html( $count ) . '</span></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights Product -> Reviews admin menu item when editing a review or a reply to a review.
|
||||
*
|
||||
* @global string $submenu_file
|
||||
*
|
||||
* @param string|mixed $parent_file Parent menu item.
|
||||
* @return string
|
||||
*/
|
||||
protected function edit_review_parent_file( $parent_file ) {
|
||||
global $submenu_file, $current_screen;
|
||||
|
||||
if ( isset( $current_screen->id, $_GET['c'] ) && $current_screen->id === 'comment' ) {
|
||||
|
||||
$comment_id = absint( $_GET['c'] );
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) {
|
||||
$comment = get_comment( $comment->comment_parent );
|
||||
}
|
||||
|
||||
if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) {
|
||||
$parent_file = 'edit.php?post_type=product';
|
||||
$submenu_file = 'product-reviews'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
}
|
||||
|
||||
return $parent_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces Edit/Moderate Comment title/headline with Edit Review, when editing/moderating a review.
|
||||
*
|
||||
* @param string|mixed $translation Translated text.
|
||||
* @param string|mixed $text Text to translate.
|
||||
* @return string|mixed Translated text.
|
||||
*/
|
||||
protected function edit_comments_screen_text( $translation, $text ) {
|
||||
global $comment;
|
||||
|
||||
// Bail out if not a text we should replace.
|
||||
if ( ! in_array( $text, [ 'Edit Comment', 'Moderate Comment' ], true ) ) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
// Try to get comment from query params when not in context already.
|
||||
if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && $_GET['action'] === 'editcomment' ) {
|
||||
$comment_id = absint( $_GET['c'] );
|
||||
$comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
$is_reply = isset( $comment->comment_parent ) && $comment->comment_parent > 0;
|
||||
|
||||
// Only replace the translated text if we are editing a comment left on a product (ie. a review).
|
||||
if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) {
|
||||
if ( $text === 'Edit Comment' ) {
|
||||
$translation = $is_reply
|
||||
? __( 'Edit Review Reply', 'woocommerce' )
|
||||
: __( 'Edit Review', 'woocommerce' );
|
||||
} elseif ( $text === 'Moderate Comment' ) {
|
||||
$translation = $is_reply
|
||||
? __( 'Moderate Review Reply', 'woocommerce' )
|
||||
: __( 'Moderate Review', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of `ReviewsListTable`, with the screen argument specified.
|
||||
*
|
||||
* @return ReviewsListTable
|
||||
*/
|
||||
protected function make_reviews_list_table() : ReviewsListTable {
|
||||
return new ReviewsListTable( [ 'screen' => $this->reviews_page_hook ? $this->reviews_page_hook : 'product_page_product-reviews' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the list table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function load_reviews_screen() : void {
|
||||
$this->reviews_list_table = $this->make_reviews_list_table();
|
||||
$this->reviews_list_table->process_bulk_action();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Reviews page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_reviews_list_table() : void {
|
||||
|
||||
$this->reviews_list_table->prepare_items();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
|
||||
|
||||
<?php $this->reviews_list_table->views(); ?>
|
||||
|
||||
<form id="reviews-filter" method="get">
|
||||
<?php $page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : static::MENU_SLUG; ?>
|
||||
|
||||
<input type="hidden" name="page" value="<?php echo esc_attr( $page ); ?>" />
|
||||
<input type="hidden" name="post_type" value="product" />
|
||||
<input type="hidden" name="pagegen_timestamp" value="<?php echo esc_attr( current_time( 'mysql', true ) ); ?>" />
|
||||
|
||||
<?php $this->reviews_list_table->search_box( __( 'Search Reviews', 'woocommerce' ), 'reviews' ); ?>
|
||||
|
||||
<?php $this->reviews_list_table->display(); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
wp_comment_reply( '-1', true, 'detail' );
|
||||
wp_comment_trashnotice();
|
||||
|
||||
/**
|
||||
* Filters the contents of the product reviews list table output.
|
||||
*
|
||||
* @since 6.7.0
|
||||
*
|
||||
* @param string $output The HTML output of the list table.
|
||||
* @param ReviewsListTable $reviews_list_table The reviews list table instance.
|
||||
*/
|
||||
echo apply_filters( 'woocommerce_product_reviews_list_table', ob_get_clean(), $this->reviews_list_table ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WP_Comment_Query;
|
||||
use WP_Screen;
|
||||
|
||||
/**
|
||||
* Tweaks the WordPress comments page to exclude reviews.
|
||||
*/
|
||||
class ReviewsCommentsOverrides {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
const REVIEWS_MOVED_NOTICE_ID = 'product_reviews_moved';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_action( 'admin_notices', array( $this, 'display_notices' ) );
|
||||
self::add_filter( 'woocommerce_dismiss_admin_notice_capability', array( $this, 'get_dismiss_capability' ), 10, 2 );
|
||||
self::add_filter( 'comments_list_table_query_args', array( $this, 'exclude_reviews_from_comments' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders admin notices.
|
||||
*/
|
||||
protected function display_notices() : void {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( empty( $screen ) || $screen->base !== 'edit-comments' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->maybe_display_reviews_moved_notice();
|
||||
}
|
||||
|
||||
/**
|
||||
* May render an admin notice informing the user that reviews were moved to a new page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function maybe_display_reviews_moved_notice() : void {
|
||||
if ( $this->should_display_reviews_moved_notice() ) {
|
||||
$this->display_reviews_moved_notice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the admin notice informing the user that reviews were moved to a new page should be displayed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_display_reviews_moved_notice() : bool {
|
||||
// Do not display if the user does not have the capability to see the new page.
|
||||
if ( ! WC()->call_function( 'current_user_can', Reviews::get_capability() ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not display if the current user has dismissed this notice.
|
||||
if ( WC()->call_function( 'get_user_meta', get_current_user_id(), 'dismissed_' . static::REVIEWS_MOVED_NOTICE_ID . '_notice', true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an admin notice informing the user that reviews were moved to a new page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function display_reviews_moved_notice() : void {
|
||||
?>
|
||||
<div class="notice notice-info is-dismissible">
|
||||
<p><strong><?php esc_html_e( 'Product reviews have moved!', 'woocommerce' ); ?></strong></p>
|
||||
<p><?php esc_html_e( 'Product reviews can now be managed from Products > Reviews.', 'woocommerce' ); ?></p>
|
||||
<p class="submit">
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&page=product-reviews' ) ); ?>" class="button-primary"><?php esc_html_e( 'Visit new location', 'woocommerce' ); ?></a>
|
||||
</p>
|
||||
|
||||
<form action="<?php echo esc_url( admin_url( 'edit-comments.php' ) ); ?>" method="get">
|
||||
<input type="hidden" name="wc-hide-notice" value="<?php echo esc_attr( static::REVIEWS_MOVED_NOTICE_ID ); ?>" />
|
||||
|
||||
<?php if ( ! empty( $_GET['comment_status'] ) ): ?>
|
||||
<input type="hidden" name="comment_status" value="<?php echo esc_attr( $_GET['comment_status'] ); ?>" />
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $_GET['paged'] ) ): ?>
|
||||
<input type="hidden" name="paged" value="<?php echo esc_attr( $_GET['paged'] ); ?>" />
|
||||
<?php endif; ?>
|
||||
|
||||
<?php wp_nonce_field( 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ); ?>
|
||||
|
||||
<button type="submit" class="notice-dismiss">
|
||||
<span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the capability required to dismiss the notice.
|
||||
*
|
||||
* This is required so that users who do not have the manage_woocommerce capability (e.g. Editors) can still dismiss
|
||||
* the notice displayed in the Comments page.
|
||||
*
|
||||
* @param string|mixed $default_capability The default required capability.
|
||||
* @param string|mixed $notice_name The notice name.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_dismiss_capability( $default_capability, $notice_name ) {
|
||||
return $notice_name === self::REVIEWS_MOVED_NOTICE_ID ? Reviews::get_capability() : $default_capability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes product reviews from showing in the comments page.
|
||||
*
|
||||
* @param array|mixed $args {@see WP_Comment_Query} query args.
|
||||
* @return array
|
||||
*/
|
||||
protected function exclude_reviews_from_comments( $args ) : array {
|
||||
$screen = get_current_screen();
|
||||
|
||||
// We only wish to intervene if the edit comments screen has been requested.
|
||||
if ( ! $screen instanceof WP_Screen || 'edit-comments' !== $screen->id ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
if ( ! empty( $args['post_type'] ) && $args['post_type'] !== 'any' ) {
|
||||
$post_types = (array) $args['post_type'];
|
||||
} else {
|
||||
$post_types = get_post_types();
|
||||
}
|
||||
|
||||
$index = array_search( 'product', $post_types );
|
||||
|
||||
if ( $index !== false ) {
|
||||
unset( $post_types[ $index ] );
|
||||
}
|
||||
|
||||
if ( ! is_array( $args ) ) {
|
||||
$args = [];
|
||||
}
|
||||
|
||||
$args['post_type'] = $post_types;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;
|
||||
|
||||
/**
|
||||
* A utility class for handling comments that are product reviews.
|
||||
*/
|
||||
class ReviewsUtil {
|
||||
|
||||
/**
|
||||
* Removes product reviews from the edit-comments page to fix the "Mine" tab counter.
|
||||
*
|
||||
* @param array|mixed $clauses A compacted array of comment query clauses.
|
||||
* @return array|mixed
|
||||
*/
|
||||
public static function comments_clauses_without_product_reviews( $clauses ) {
|
||||
global $wpdb, $current_screen;
|
||||
|
||||
if ( isset( $current_screen->base ) && 'edit-comments' === $current_screen->base ) {
|
||||
$clauses['join'] .= " LEFT JOIN {$wpdb->posts} AS wp_posts_to_exclude_reviews ON comment_post_ID = wp_posts_to_exclude_reviews.ID ";
|
||||
$clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " wp_posts_to_exclude_reviews.post_type NOT IN ('product') ";
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
<?php
|
||||
/**
|
||||
* Gets a list of fallback methods if remote fetching is disabled.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
||||
/**
|
||||
* Default Free Extensions
|
||||
*/
|
||||
class DefaultFreeExtensions {
|
||||
|
||||
/**
|
||||
* Get default specs.
|
||||
*
|
||||
* @return array Default specs.
|
||||
*/
|
||||
public static function get_all() {
|
||||
$bundles = array(
|
||||
array(
|
||||
'key' => 'obw/basics',
|
||||
'title' => __( 'Get the basics', 'woocommerce' ),
|
||||
'plugins' => array(
|
||||
self::get_plugin( 'woocommerce-payments' ),
|
||||
self::get_plugin( 'woocommerce-services:shipping' ),
|
||||
self::get_plugin( 'woocommerce-services:tax' ),
|
||||
self::get_plugin( 'jetpack' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'key' => 'obw/grow',
|
||||
'title' => __( 'Grow your store', 'woocommerce' ),
|
||||
'plugins' => array(
|
||||
self::get_plugin( 'mailpoet' ),
|
||||
self::get_plugin( 'google-listings-and-ads' ),
|
||||
self::get_plugin( 'pinterest-for-woocommerce' ),
|
||||
self::get_plugin( 'facebook-for-woocommerce' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'key' => 'task-list/reach',
|
||||
'title' => __( 'Reach out to customers', 'woocommerce' ),
|
||||
'plugins' => array(
|
||||
self::get_plugin( 'mailpoet:alt' ),
|
||||
self::get_plugin( 'mailchimp-for-woocommerce' ),
|
||||
self::get_plugin( 'klaviyo' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'key' => 'task-list/grow',
|
||||
'title' => __( 'Grow your store', 'woocommerce' ),
|
||||
'plugins' => array(
|
||||
self::get_plugin( 'google-listings-and-ads:alt' ),
|
||||
self::get_plugin( 'tiktok-for-business' ),
|
||||
self::get_plugin( 'pinterest-for-woocommerce:alt' ),
|
||||
self::get_plugin( 'facebook-for-woocommerce:alt' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'key' => 'obw/core-profiler',
|
||||
'title' => __( 'Grow your store', 'woocommerce' ),
|
||||
'plugins' => self::with_core_profiler_fields(
|
||||
array(
|
||||
self::get_plugin( 'woocommerce-payments' ),
|
||||
self::get_plugin( 'woocommerce-services:shipping' ),
|
||||
self::get_plugin( 'jetpack' ),
|
||||
self::get_plugin( 'pinterest-for-woocommerce' ),
|
||||
self::get_plugin( 'mailpoet' ),
|
||||
self::get_plugin( 'google-listings-and-ads' ),
|
||||
self::get_plugin( 'woocommerce-services:tax' ),
|
||||
self::get_plugin( 'tiktok-for-business' ),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$bundles = wp_json_encode( $bundles );
|
||||
return json_decode( $bundles );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin arguments by slug.
|
||||
*
|
||||
* @param string $slug Slug.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_plugin( $slug ) {
|
||||
$plugins = array(
|
||||
'google-listings-and-ads' => array(
|
||||
'min_php_version' => '7.4',
|
||||
'name' => __( 'Google Listings & Ads', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/google-listings-and-ads" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
|
||||
'is_built_by_wc' => true,
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'not',
|
||||
'operand' => array(
|
||||
array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => array( 'google-listings-and-ads' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'google-listings-and-ads:alt' => array(
|
||||
'name' => __( 'Google Listings & Ads', 'woocommerce' ),
|
||||
'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'facebook-for-woocommerce' => array(
|
||||
'name' => __( 'Facebook for WooCommerce', 'woocommerce' ),
|
||||
'description' => __( 'List products and create ads on Facebook and Instagram with <a href="https://woocommerce.com/products/facebook/">Facebook for WooCommerce</a>', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-facebook',
|
||||
'is_visible' => false,
|
||||
'is_built_by_wc' => false,
|
||||
),
|
||||
'facebook-for-woocommerce:alt' => array(
|
||||
'name' => __( 'Facebook for WooCommerce', 'woocommerce' ),
|
||||
'description' => __( 'List products and create ads on Facebook and Instagram.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-facebook',
|
||||
'is_visible' => false,
|
||||
'is_built_by_wc' => false,
|
||||
),
|
||||
'pinterest-for-woocommerce' => array(
|
||||
'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
|
||||
'description' => __( 'Get your products in front of Pinners searching for ideas and things to buy.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
|
||||
'is_built_by_wc' => true,
|
||||
'min_php_version' => '7.3',
|
||||
),
|
||||
'pinterest-for-woocommerce:alt' => array(
|
||||
'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
|
||||
'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'mailpoet' => array(
|
||||
'name' => __( 'MailPoet', 'woocommerce' ),
|
||||
'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=mailpoet-newsletters',
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'mailchimp-for-woocommerce' => array(
|
||||
'name' => __( 'Mailchimp', 'woocommerce' ),
|
||||
'description' => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=mailchimp-woocommerce',
|
||||
'is_built_by_wc' => false,
|
||||
),
|
||||
'klaviyo' => array(
|
||||
'name' => __( 'Klaviyo', 'woocommerce' ),
|
||||
'description' => __( 'Grow and retain customers with intelligent, impactful email and SMS marketing automation and a consolidated view of customer interactions.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/klaviyo.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=klaviyo_settings',
|
||||
'is_built_by_wc' => false,
|
||||
),
|
||||
'woocommerce-payments' => array(
|
||||
'name' => __( 'WooPayments', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/wcpay.svg', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Accept credit cards and other popular payment methods with %1$sWooPayments%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/woocommerce-payments" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'or',
|
||||
'operands' => array(
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'US',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CA',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'ES',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'FR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GB',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'IE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'IT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NZ',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'BE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NL',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PL',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CH',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'HK',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SG',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CY',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DK',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'EE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'FI',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'LU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'LT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'LV',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NO',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'MT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SI',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SK',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'BG',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CZ',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'HR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'HU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'RO',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'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 ),
|
||||
),
|
||||
'is_built_by_wc' => true,
|
||||
'min_wp_version' => '5.9',
|
||||
),
|
||||
'woocommerce-services:shipping' => array(
|
||||
'name' => __( 'WooCommerce Shipping', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Print shipping labels with %1$sWooCommerce Shipping%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/shipping" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'US',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'not',
|
||||
'operand' => array(
|
||||
array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => array( 'woocommerce-services' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'type' => 'or',
|
||||
'operands' => array(
|
||||
array(
|
||||
array(
|
||||
'type' => 'option',
|
||||
'transformers' => array(
|
||||
array(
|
||||
'use' => 'dot_notation',
|
||||
'arguments' => array(
|
||||
'path' => 'product_types',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'use' => 'count',
|
||||
),
|
||||
),
|
||||
'option_name' => 'woocommerce_onboarding_profile',
|
||||
'value' => 1,
|
||||
'default' => array(),
|
||||
'operation' => '!=',
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'type' => 'option',
|
||||
'transformers' => array(
|
||||
array(
|
||||
'use' => 'dot_notation',
|
||||
'arguments' => array(
|
||||
'path' => 'product_types.0',
|
||||
),
|
||||
),
|
||||
),
|
||||
'option_name' => 'woocommerce_onboarding_profile',
|
||||
'value' => 'downloads',
|
||||
'default' => '',
|
||||
'operation' => '!=',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'woocommerce-services:tax' => array(
|
||||
'name' => __( 'WooCommerce Tax', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Get automated sales tax with %1$sWooCommerce Tax%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/tax" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'or',
|
||||
'operands' => array(
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'US',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'FR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GB',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CA',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'BE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DK',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SE',
|
||||
'operation' => '=',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'type' => 'not',
|
||||
'operand' => array(
|
||||
array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => array( 'woocommerce-services' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'jetpack' => array(
|
||||
'name' => __( 'Jetpack', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/jetpack.svg', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Enhance speed and security with %1$sJetpack%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/jetpack" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'not',
|
||||
'operand' => array(
|
||||
array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => array( 'jetpack' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'is_built_by_wc' => false,
|
||||
'min_wp_version' => '6.0',
|
||||
),
|
||||
'mailpoet' => array(
|
||||
'name' => __( 'MailPoet', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Level up your email marketing with %1$sMailPoet%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/mailpoet" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'manage_url' => 'admin.php?page=mailpoet-newsletters',
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'not',
|
||||
'operand' => array(
|
||||
array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => array( 'mailpoet' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'mailpoet:alt' => array(
|
||||
'name' => __( 'MailPoet', 'woocommerce' ),
|
||||
'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=mailpoet-newsletters',
|
||||
'is_built_by_wc' => true,
|
||||
),
|
||||
'tiktok-for-business' => array(
|
||||
'name' => __( 'TikTok for WooCommerce', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ),
|
||||
'description' =>
|
||||
__( 'Grow your online sales by promoting your products on TikTok to over one billion monthly active users around the world.', 'woocommerce' ),
|
||||
'manage_url' => 'admin.php?page=tiktok',
|
||||
'is_visible' => array(
|
||||
array(
|
||||
'type' => 'or',
|
||||
'operands' => array(
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'US',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CA',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'MX',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'BE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CZ',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DK',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'FI',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'FR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'DE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'HU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'IE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'IT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NL',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PL',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PT',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'RO',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'ES',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'GB',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'CH',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NO',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'NZ',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SG',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'MY',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'PH',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'ID',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'VN',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'TH',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'KR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'IL',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'RU',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'UA',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'TR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'SA',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'BR',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'JP',
|
||||
'operation' => '=',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'is_built_by_wc' => false,
|
||||
),
|
||||
'tiktok-for-business:alt' => array(
|
||||
'name' => __( 'TikTok for WooCommerce', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
__( 'Create ad campaigns and reach one billion global users with %1$sTikTok for WooCommerce%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/products/tiktok-for-woocommerce" target="_blank">',
|
||||
'</a>'
|
||||
),
|
||||
'manage_url' => 'admin.php?page=tiktok',
|
||||
'is_built_by_wc' => false,
|
||||
'is_visible' => false,
|
||||
),
|
||||
);
|
||||
|
||||
$plugin = $plugins[ $slug ];
|
||||
$plugin['key'] = $slug;
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate plugin data with core profiler fields.
|
||||
*
|
||||
* - Updated description for the core-profiler.
|
||||
* - Adds learn_more_link and label.
|
||||
* - Adds install_priority, which is used to sort the plugins. The value is determined by the plugin size. Lower = smaller.
|
||||
*
|
||||
* @param array $plugins Array of plugins.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function with_core_profiler_fields( array $plugins ) {
|
||||
$_plugins = array(
|
||||
'woocommerce-payments' => array(
|
||||
'label' => __( 'Get paid with WooPayments', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments',
|
||||
'install_priority' => 5,
|
||||
),
|
||||
'woocommerce-services:shipping' => array(
|
||||
'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping',
|
||||
'install_priority' => 3,
|
||||
),
|
||||
'jetpack' => array(
|
||||
'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ),
|
||||
'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,
|
||||
),
|
||||
'pinterest-for-woocommerce' => array(
|
||||
'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-pinterest.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce',
|
||||
'install_priority' => 2,
|
||||
),
|
||||
'mailpoet' => array(
|
||||
'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-mailpoet.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/mailpoet',
|
||||
'install_priority' => 7,
|
||||
),
|
||||
'tiktok-for-business' => array(
|
||||
'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-tiktok.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce',
|
||||
'install_priority' => 1,
|
||||
),
|
||||
'google-listings-and-ads' => array(
|
||||
'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads',
|
||||
'install_priority' => 6,
|
||||
),
|
||||
'woocommerce-services:tax' => array(
|
||||
'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/tax',
|
||||
'install_priority' => 4,
|
||||
),
|
||||
);
|
||||
|
||||
// Copy shipping for the core-profiler and remove is_visible conditions, except for the country restriction.
|
||||
$_plugins['woocommerce-services:shipping']['is_visible'] = [
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'US',
|
||||
'operation' => '=',
|
||||
),
|
||||
];
|
||||
|
||||
$remove_plugins_activated_rule = function( $is_visible ) {
|
||||
$is_visible = array_filter(
|
||||
array_map(
|
||||
function( $rule ) {
|
||||
if ( is_object( $rule ) || ! isset( $rule['operand'] ) ) {
|
||||
return $rule;
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$rule['operand'],
|
||||
function( $operand ) {
|
||||
return 'plugins_activated' !== $operand['type'];
|
||||
}
|
||||
);
|
||||
},
|
||||
$is_visible
|
||||
)
|
||||
);
|
||||
|
||||
return empty( $is_visible ) ? true : $is_visible;
|
||||
};
|
||||
|
||||
foreach ( $plugins as &$plugin ) {
|
||||
if ( isset( $_plugins[ $plugin['key'] ] ) ) {
|
||||
$plugin = array_merge( $plugin, $_plugins[ $plugin['key'] ] );
|
||||
if ( isset( $plugin['is_visible'] ) && is_array( $plugin['is_visible'] ) ) {
|
||||
$plugin['is_visible'] = $remove_plugins_activated_rule( $plugin['is_visible'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Evaluates the spec and returns a status.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator;
|
||||
|
||||
/**
|
||||
* Evaluates the extension and returns it.
|
||||
*/
|
||||
class EvaluateExtension {
|
||||
/**
|
||||
* Evaluates the extension and returns it.
|
||||
*
|
||||
* @param object $extension The extension to evaluate.
|
||||
* @return object The evaluated extension.
|
||||
*/
|
||||
private static function evaluate( $extension ) {
|
||||
global $wp_version;
|
||||
$rule_evaluator = new RuleEvaluator();
|
||||
|
||||
if ( isset( $extension->is_visible ) ) {
|
||||
$is_visible = $rule_evaluator->evaluate( $extension->is_visible );
|
||||
$extension->is_visible = $is_visible;
|
||||
} else {
|
||||
$extension->is_visible = true;
|
||||
}
|
||||
|
||||
// Run PHP and WP version chcecks.
|
||||
if ( true === $extension->is_visible ) {
|
||||
if ( isset( $extension->min_php_version ) && ! version_compare( PHP_VERSION, $extension->min_php_version, '>=' ) ) {
|
||||
$extension->is_visible = false;
|
||||
}
|
||||
|
||||
if ( isset( $extension->min_wp_version ) && ! version_compare( $wp_version, $extension->min_wp_version, '>=' ) ) {
|
||||
$extension->is_visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
$activated_plugins = PluginsHelper::get_active_plugin_slugs();
|
||||
$extension->is_installed = in_array( explode( ':', $extension->key )[0], $installed_plugins, true );
|
||||
$extension->is_activated = in_array( explode( ':', $extension->key )[0], $activated_plugins, true );
|
||||
|
||||
return $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the specs and returns the bundles with visible extensions.
|
||||
*
|
||||
* @param array $specs extensions spec array.
|
||||
* @param array $allowed_bundles Optional array of allowed bundles to be returned.
|
||||
* @return array The bundles and errors.
|
||||
*/
|
||||
public static function evaluate_bundles( $specs, $allowed_bundles = array() ) {
|
||||
$bundles = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
$spec = (object) $spec;
|
||||
$bundle = (array) $spec;
|
||||
$bundle['plugins'] = array();
|
||||
|
||||
if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$errors = array();
|
||||
foreach ( $spec->plugins as $plugin ) {
|
||||
try {
|
||||
$extension = self::evaluate( (object) $plugin );
|
||||
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
|
||||
$bundle['plugins'][] = $extension;
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
|
||||
return array(
|
||||
'bundles' => $bundles,
|
||||
'errors' => $errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles running payment method specs
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\DefaultFreeExtensions;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* Remote Payment Methods engine.
|
||||
* This goes through the specs and gets eligible payment methods.
|
||||
*/
|
||||
class Init extends RemoteSpecsEngine {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the specs and run them.
|
||||
*
|
||||
* @param array $allowed_bundles Optional array of allowed bundles to be returned.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_extensions( $allowed_bundles = array() ) {
|
||||
$locale = get_user_locale();
|
||||
|
||||
$specs = self::get_specs();
|
||||
$results = EvaluateExtension::evaluate_bundles( $specs, $allowed_bundles );
|
||||
$specs_to_return = $results['bundles'];
|
||||
$specs_to_save = null;
|
||||
|
||||
$plugins = array_filter(
|
||||
$results['bundles'],
|
||||
function( $bundle ) {
|
||||
return count( $bundle['plugins'] ) > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $plugins ) ) {
|
||||
// When no plugins are visible, replace it with defaults and save for 3 hours.
|
||||
$specs_to_save = DefaultFreeExtensions::get_all();
|
||||
$specs_to_return = EvaluateExtension::evaluate_bundles( $specs_to_save, $allowed_bundles )['bundles'];
|
||||
} elseif ( count( $results['errors'] ) > 0 ) {
|
||||
// When suggestions is not empty but has errors, save it for 3 hours.
|
||||
$specs_to_save = $specs;
|
||||
}
|
||||
|
||||
// When plugins is not empty but has errors, save it for 3 hours.
|
||||
if ( count( $results['errors'] ) > 0 ) {
|
||||
self::log_errors( $results['errors'] );
|
||||
}
|
||||
|
||||
if ( $specs_to_save ) {
|
||||
RemoteFreeExtensionsDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs_to_save ), 3 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $specs_to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specs transient.
|
||||
*/
|
||||
public static function delete_specs_transient() {
|
||||
RemoteFreeExtensionsDataSourcePoller::get_instance()->delete_specs_transient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specs or fetch remotely if they don't exist.
|
||||
*/
|
||||
public static function get_specs() {
|
||||
if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
|
||||
return DefaultFreeExtensions::get_all();
|
||||
}
|
||||
$specs = RemoteFreeExtensionsDataSourcePoller::get_instance()->get_specs_from_data_sources();
|
||||
|
||||
// Fetch specs if they don't yet exist.
|
||||
if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
|
||||
return DefaultFreeExtensions::get_all();
|
||||
}
|
||||
|
||||
return $specs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
|
||||
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\DataSourcePoller;
|
||||
/**
|
||||
* Specs data source poller class for remote free extensions.
|
||||
*/
|
||||
class RemoteFreeExtensionsDataSourcePoller extends DataSourcePoller {
|
||||
|
||||
const ID = 'remote_free_extensions';
|
||||
|
||||
const DATA_SOURCES = array(
|
||||
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/4.0/extensions.json',
|
||||
);
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Analytics instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self(
|
||||
self::ID,
|
||||
self::DATA_SOURCES,
|
||||
array(
|
||||
'spec_key' => 'key',
|
||||
)
|
||||
);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Remote Inbox Notifications feature.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
|
||||
|
||||
/**
|
||||
* Remote Inbox Notifications feature logic.
|
||||
*/
|
||||
class RemoteInboxNotifications {
|
||||
/**
|
||||
* Option name used to toggle this feature.
|
||||
*/
|
||||
const TOGGLE_OPTION_NAME = 'woocommerce_show_marketplace_suggestions';
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var RemoteInboxNotifications instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WooCommerce.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Features::is_enabled( 'remote-inbox-notifications' ) ) {
|
||||
RemoteInboxNotificationsEngine::init();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Customer syncing related functions and actions.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Schedulers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
|
||||
|
||||
/**
|
||||
* CustomersScheduler Class.
|
||||
*/
|
||||
class CustomersScheduler extends ImportScheduler {
|
||||
/**
|
||||
* Slug to identify the scheduler.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $name = 'customers';
|
||||
|
||||
/**
|
||||
* Attach customer lookup update hooks.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function init() {
|
||||
CustomersDataStore::init();
|
||||
parent::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add customer dependencies.
|
||||
*
|
||||
* @internal
|
||||
* @return array
|
||||
*/
|
||||
public static function get_dependencies() {
|
||||
return array(
|
||||
'delete_batch_init' => OrdersScheduler::get_action( 'delete_batch_init' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer IDs and total count that need to be synced.
|
||||
*
|
||||
* @internal
|
||||
* @param int $limit Number of records to retrieve.
|
||||
* @param int $page Page number.
|
||||
* @param int|bool $days Number of days prior to current date to limit search results.
|
||||
* @param bool $skip_existing Skip already imported customers.
|
||||
*/
|
||||
public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
|
||||
$customer_roles = apply_filters( 'woocommerce_analytics_import_customer_roles', array( 'customer' ) );
|
||||
$query_args = array(
|
||||
'fields' => 'ID',
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'number' => $limit,
|
||||
'paged' => $page,
|
||||
'role__in' => $customer_roles,
|
||||
);
|
||||
|
||||
if ( is_int( $days ) ) {
|
||||
$query_args['date_query'] = array(
|
||||
'after' => gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( $skip_existing ) {
|
||||
add_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );
|
||||
}
|
||||
|
||||
$customer_query = new \WP_User_Query( $query_args );
|
||||
|
||||
remove_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) );
|
||||
|
||||
return (object) array(
|
||||
'total' => $customer_query->get_total(),
|
||||
'ids' => $customer_query->get_results(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude users that already exist in our customer lookup table.
|
||||
*
|
||||
* Meant to be hooked into 'pre_user_query' action.
|
||||
*
|
||||
* @internal
|
||||
* @param WP_User_Query $wp_user_query WP_User_Query to modify.
|
||||
*/
|
||||
public static function exclude_existing_customers_from_query( $wp_user_query ) {
|
||||
global $wpdb;
|
||||
|
||||
$wp_user_query->query_where .= " AND NOT EXISTS (
|
||||
SELECT ID FROM {$wpdb->prefix}wc_customer_lookup
|
||||
WHERE {$wpdb->prefix}wc_customer_lookup.user_id = {$wpdb->users}.ID
|
||||
)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of rows imported.
|
||||
*
|
||||
* @internal
|
||||
* @return int
|
||||
*/
|
||||
public static function get_total_imported() {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a single customer.
|
||||
*
|
||||
* @internal
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function import( $user_id ) {
|
||||
CustomersDataStore::update_registered_customer( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of customers.
|
||||
*
|
||||
* @internal
|
||||
* @param int $batch_size Number of items to delete.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete( $batch_size ) {
|
||||
global $wpdb;
|
||||
|
||||
$customer_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT customer_id FROM {$wpdb->prefix}wc_customer_lookup ORDER BY customer_id ASC LIMIT %d",
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $customer_ids as $customer_id ) {
|
||||
CustomersDataStore::delete_customer( $customer_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user