';
}
/**
* Displays a review type drop-down for filtering reviews in the Product Reviews list table.
*
* @see WP_Comments_List_Table::comment_type_dropdown() for consistency.
*
* @param string|mixed $current_type The current comment item type slug.
* @return void
*/
protected function review_type_dropdown( $current_type ) : void {
/**
* Sets the possible options used in the Product Reviews List Table's filter-by-review-type
* selector.
*
* @since 7.0.0
*
* @param array Map of possible review types.
*/
$item_types = apply_filters(
'woocommerce_product_reviews_list_table_item_types',
array(
'all' => __( 'All types', 'woocommerce' ),
'comment' => __( 'Replies', 'woocommerce' ),
'review' => __( 'Reviews', 'woocommerce' ),
)
);
?>
__( 'All ratings', 'woocommerce' ),
'1' => '★',
'2' => '★★',
'3' => '★★★',
'4' => '★★★★',
'5' => '★★★★★',
];
?>
—%s',
esc_html__( 'No reviews', 'woocommerce' )
);
} elseif ( $approved_review_count && 'trash' === get_post_status( $post_id ) ) {
// Don't link the comment bubble for a trashed product.
printf(
'%s%s',
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
);
} elseif ( $approved_review_count ) {
// Link the comment bubble to approved reviews.
printf(
'%s%s',
esc_url(
add_query_arg(
[
'product_id' => urlencode( $post_id ),
'comment_status' => 'approved',
],
Reviews::get_reviews_page_url()
)
),
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
);
} else {
// Don't link the comment bubble when there are no approved reviews.
printf(
'%s%s',
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html__( 'No approved reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
);
}
if ( $pending_comments ) {
printf(
'%s%s',
esc_url(
add_query_arg(
[
'product_id' => urlencode( $post_id ),
'comment_status' => 'moderated',
],
Reviews::get_reviews_page_url()
)
),
esc_html( $pending_reviews_number ),
esc_html( $pending_phrase )
);
} else {
printf(
'%s%s',
esc_html( $pending_reviews_number ),
$approved_review_count ? esc_html__( 'No pending reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
);
}
}
}
ProductReviews/ReviewsCommentsOverrides.php 0000644 00000011354 15073235522 0015267 0 ustar 00 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 {
?>
Reviews.', 'woocommerce' ); ?>
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;
}
}
Marketplace.php 0000644 00000011550 15073235522 0007513 0 ustar 00 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' ),
'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() {
?>
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',
[ $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;
}
$rtl = is_rtl() ? '-rtl' : '';
wp_enqueue_style(
'wc-admin-marketing-coupons',
WCAdminAssets::get_url( "marketing-coupons/style{$rtl}", 'css' ),
array(),
WCAdminAssets::get_file_version( 'css' )
);
WCAdminAssets::register_script( 'wp-admin-scripts', 'marketing-coupons', true );
}
}
Marketing.php 0000644 00000007404 15073235522 0007207 0 ustar 00 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];
}
}
}
/**
* 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;
}
}
Homescreen.php 0000644 00000021275 15073235522 0007360 0 ustar 00 =' ) ) {
// 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;
}
}
Notes/OrderMilestones.php 0000644 00000022206 15073235522 0011471 0 ustar 00 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://woo.com/document/managing-orders/?utm_source=inbox&utm_medium=product',
);
case 10:
return array(
'name' => 'browse',
'label' => __( 'Browse', 'woocommerce' ),
'query' => 'https://woo.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();
}
}
Notes/GivingFeedbackNotes.php 0000644 00000003002 15073235522 0012205 0 ustar 00 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;
}
}
Notes/MerchantEmailNotifications.php 0000644 00000006601 15073235522 0013617 0 ustar 00 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 );
}
}
Notes/FirstProduct.php 0000644 00000004175 15073235522 0011010 0 ustar 00 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;
}
}
Notes/CustomizingProductCatalog.php 0000644 00000004205 15073235522 0013521 0 ustar 00 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://woo.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product'
);
return $note;
}
}
Notes/LaunchChecklist.php 0000644 00000003260 15073235522 0011416 0 ustar 00 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://woo.com/posts/pre-launch-checklist-the-essentials/?utm_source=inbox&utm_medium=product' );
return $note;
}
}
Notes/WooCommerceSubscriptions.php 0000644 00000003572 15073235522 0013367 0 ustar 00 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://woo.com/products/woocommerce-subscriptions/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_UNACTIONED,
true
);
return $note;
}
}
Notes/WooCommercePayments.php 0000644 00000014370 15073235522 0012316 0 ustar 00 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 WooPayments.', 'woocommerce' ) .
'
' .
sprintf(
/* translators: 1: opening link tag, 2: closing tag */
__( 'By clicking "Get started", you agree to our %1$sTerms of Service%2$s', '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://woo.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;
}
}
Notes/TrackingOptIn.php 0000644 00000005355 15073235522 0011075 0 ustar 00 ',
'',
''
);
$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 ) );
}
}
}
Notes/MarketingJetpack.php 0000644 00000007255 15073235522 0011605 0 ustar 00 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 );
}
}
Notes/PaymentsRemindMeLater.php 0000644 00000003676 15073235522 0012576 0 ustar 00 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;
}
}
Notes/PerformanceOnMobile.php 0000644 00000003205 15073235522 0012237 0 ustar 00 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://woo.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;
}
}
Notes/PaymentsMoreInfoNeeded.php 0000644 00000004000 15073235522 0012707 0 ustar 00 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://woo.com/payments/' );
return $note;
}
}
Notes/WooSubscriptionsNotes.php 0000644 00000034346 15073235522 0012730 0 ustar 00 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 Woo.com.
*
* @return bool
*/
public function is_connected() {
$auth = \WC_Helper_Options::get( 'auth' );
return ( ! empty( $auth['access_token'] ) );
}
/**
* Returns the Woo.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 Woo.com.
*/
public function add_no_connection_note() {
$note = self::get_note();
$note->save();
}
/**
* Get the Woo.com connection note
*/
public static function get_note() {
$note = new Note();
$note->set_title( __( 'Connect to Woo.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://woo.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 );
}
}
}
Notes/NewSalesRecord.php 0000644 00000012404 15073235522 0011232 0 ustar 00 $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
);
}
}
Notes/InstallJPAndWCSPlugins.php 0000644 00000011122 15073235522 0012550 0 ustar 00 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();
}
}
Notes/EUVATNumber.php 0000644 00000003202 15073235522 0010403 0 ustar 00 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://woo.com/products/eu-vat-number/?utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;
}
}
Notes/MigrateFromShopify.php 0000644 00000004320 15073235522 0012126 0 ustar 00 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://woo.com/posts/migrate-from-shopify-to-woocommerce/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;
}
}
Notes/SellingOnlineCourses.php 0000644 00000004570 15073235522 0012465 0 ustar 00 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://woo.com/posts/how-to-sell-online-courses-wordpress/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;
}
}
Notes/UnsecuredReportFiles.php 0000644 00000004156 15073235522 0012473 0 ustar 00 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' ),
'',
''
)
);
$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.woo.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 );
}
}
Notes/PersonalizeStore.php 0000644 00000003646 15073235522 0011672 0 ustar 00 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;
}
}
Notes/AddFirstProduct.php 0000644 00000005435 15073235522 0011421 0 ustar 00 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}
',
/* 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' ), '
' ),
__( 'There are three ways to add your products: you can create products manually, import them at once via CSV file, or migrate them from another service.
', 'woocommerce' ),
/* translators: %1$s is an open anchor tag () and %2$s is a close link tag (). */
sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '', '' ),
);
$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;
}
}
Notes/EmailNotification.php 0000644 00000012250 15073235522 0011747 0 ustar 00 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() ) );
}
}
Notes/OnboardingPayments.php 0000644 00000003354 15073235522 0012161 0 ustar 00 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://woo.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED,
true
);
return $note;
}
}
Notes/CustomizeStoreWithBlocks.php 0000644 00000004533 15073235522 0013347 0 ustar 00 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://woo.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;
}
}
Notes/MobileApp.php 0000644 00000002605 15073235522 0010224 0 ustar 00 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://woo.com/mobile/?utm_medium=product' );
return $note;
}
}
Notes/CouponPageMoved.php 0000644 00000007463 15073235522 0011416 0 ustar 00 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 );
}
}
Notes/ManageOrdersOnTheGo.php 0000644 00000003041 15073235522 0012142 0 ustar 00 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://woo.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;
}
}
Notes/ChoosingTheme.php 0000644 00000002676 15073235522 0011120 0 ustar 00 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://woo.com/product-category/themes/?utm_source=inbox&utm_medium=product'
);
return $note;
}
}
Notes/OnlineClothingStore.php 0000644 00000005261 15073235522 0012306 0 ustar 00 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://woo.com/posts/starting-an-online-clothing-store/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;
}
}
Notes/MagentoMigration.php 0000644 00000004704 15073235522 0011622 0 ustar 00 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://woo.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox'
);
return $note;
}
}
Notes/RealTimeOrderAlerts.php 0000644 00000003002 15073235522 0012215 0 ustar 00 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://woo.com/mobile/?utm_source=inbox&utm_medium=product' );
return $note;
}
}
Notes/EditProductsOnTheMove.php 0000644 00000003254 15073235522 0012553 0 ustar 00 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://woo.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;
}
}
Schedulers/MailchimpScheduler.php 0000644 00000007160 15073235522 0013130 0 ustar 00 logger = $logger;
}
/**
* Attempt to subscribe store_email to MailChimp.
*
* @internal
*/
public function run() {
// Abort if we've already subscribed to MailChimp.
if ( 'yes' === get_option( self::SUBSCRIBED_OPTION_NAME ) ) {
return false;
}
$profile_data = get_option( 'woocommerce_onboarding_profile' );
if ( ! isset( $profile_data['is_agree_marketing'] ) || false === $profile_data['is_agree_marketing'] ) {
return false;
}
// Abort if store_email doesn't exist.
if ( ! isset( $profile_data['store_email'] ) ) {
return false;
}
// Abort if failed requests reaches the threshold.
if ( intval( get_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, 0 ) ) >= self::MAX_ERROR_THRESHOLD ) {
return false;
}
$response = $this->make_request( $profile_data['store_email'] );
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
$this->handle_request_error();
return false;
}
$body = json_decode( $response['body'] );
if ( isset( $body->success ) && true === $body->success ) {
update_option( self::SUBSCRIBED_OPTION_NAME, 'yes' );
return true;
}
$this->handle_request_error( $body );
return false;
}
/**
* Make an HTTP request to the API.
*
* @internal
* @param string $store_email Email address to subscribe.
*
* @return mixed
*/
public function make_request( $store_email ) {
if ( true === defined( 'WP_ENVIRONMENT_TYPE' ) && 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
} else {
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT;
}
return wp_remote_post(
$subscribe_endpoint,
array(
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
'method' => 'POST',
'body' => array(
'email' => $store_email,
),
)
);
}
/**
* Reset options.
*
* @internal
*/
public static function reset() {
delete_option( self::SUBSCRIBED_OPTION_NAME );
delete_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME );
}
/**
* Handle subscribe API error.
*
* @internal
* @param string $extra_msg Extra message to log.
*/
private function handle_request_error( $extra_msg = null ) {
// phpcs:ignore
$msg = isset( $extra_msg ) ? 'Incorrect response from Mailchimp API with: ' . print_r( $extra_msg, true ) : 'Error getting a response from Mailchimp API.';
$this->logger->error( $msg, array( 'source' => self::LOGGER_CONTEXT ) );
$accumulated_error_count = intval( get_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, 0 ) ) + 1;
update_option( self::SUBSCRIBED_ERROR_COUNT_OPTION_NAME, $accumulated_error_count );
}
}
Schedulers/OrdersScheduler.php 0000644 00000020647 15073235522 0012470 0 ustar 00 \Automattic\WooCommerce\Internal\Admin\Schedulers\CustomersScheduler::get_action( 'import_batch_init' ),
);
}
/**
* Get the order/refund 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 orders.
*/
public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) {
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
return self::get_items_from_orders_table( $limit, $page, $days, $skip_existing );
} else {
return self::get_items_from_posts_table( $limit, $page, $days, $skip_existing );
}
}
/**
* Helper method to ger order/refund IDS and total count that needs 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 orders.
*
* @return object Total counts.
*/
private static function get_items_from_posts_table( $limit, $page, $days, $skip_existing ) {
global $wpdb;
$where_clause = '';
$offset = $page > 1 ? ( $page - 1 ) * $limit : 0;
if ( is_int( $days ) ) {
$days_ago = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
$where_clause .= " AND post_date_gmt >= '{$days_ago}'";
}
if ( $skip_existing ) {
$where_clause .= " AND NOT EXISTS (
SELECT 1 FROM {$wpdb->prefix}wc_order_stats
WHERE {$wpdb->prefix}wc_order_stats.order_id = {$wpdb->posts}.ID
)";
}
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_type IN ( 'shop_order', 'shop_order_refund' )
AND post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
{$where_clause}"
); // phpcs:ignore unprepared SQL ok.
$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type IN ( 'shop_order', 'shop_order_refund' )
AND post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
{$where_clause}
ORDER BY post_date_gmt ASC
LIMIT %d
OFFSET %d",
$limit,
$offset
)
) : array(); // phpcs:ignore unprepared SQL ok.
return (object) array(
'total' => absint( $count ),
'ids' => $order_ids,
);
}
/**
* Helper method to ger order/refund IDS and total count that needs to be synced from HPOS.
*
* @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 orders.
*
* @return object Total counts.
*/
private static function get_items_from_orders_table( $limit, $page, $days, $skip_existing ) {
global $wpdb;
$where_clause = '';
$offset = $page > 1 ? ( $page - 1 ) * $limit : 0;
$order_table = OrdersTableDataStore::get_orders_table_name();
if ( is_int( $days ) ) {
$days_ago = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) );
$where_clause .= " AND orders.date_created_gmt >= '{$days_ago}'";
}
if ( $skip_existing ) {
$where_clause .= "AND NOT EXiSTS (
SELECT 1 FROM {$wpdb->prefix}wc_order_stats
WHERE {$wpdb->prefix}wc_order_stats.order_id = orders.id
)
";
}
$count = $wpdb->get_var(
"
SELECT COUNT(*) FROM {$order_table} AS orders
WHERE type in ( 'shop_order', 'shop_order_refund' )
AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
{$where_clause}
"
); // phpcs:ignore unprepared SQL ok.
$order_ids = absint( $count ) > 0 ? $wpdb->get_col(
$wpdb->prepare(
"SELECT id FROM {$order_table} AS orders
WHERE type IN ( 'shop_order', 'shop_order_refund' )
AND status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
{$where_clause}
ORDER BY date_created_gmt ASC
LIMIT %d
OFFSET %d",
$limit,
$offset
)
) : array(); // phpcs:ignore unprepared SQL ok.
return (object) array(
'total' => absint( $count ),
'ids' => $order_ids,
);
}
/**
* Get total number of rows imported.
*
* @internal
*/
public static function get_total_imported() {
global $wpdb;
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_order_stats" );
}
/**
* Schedule this import if the post is an order or refund.
*
* @param int $order_id Post ID.
*
* @internal
* @returns int The order id
*/
public static function possibly_schedule_import( $order_id ) {
if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) {
return $order_id;
}
self::schedule_action( 'import', array( $order_id ) );
return $order_id;
}
/**
* Imports a single order or refund to update lookup tables for.
* If an error is encountered in one of the updates, a retry action is scheduled.
*
* @internal
* @param int $order_id Order or refund ID.
* @return void
*/
public static function import( $order_id ) {
$order = wc_get_order( $order_id );
// If the order isn't found for some reason, skip the sync.
if ( ! $order ) {
return;
}
$type = $order->get_type();
// If the order isn't the right type, skip sync.
if ( 'shop_order' !== $type && 'shop_order_refund' !== $type ) {
return;
}
// If the order has no id or date created, skip sync.
if ( ! $order->get_id() || ! $order->get_date_created() ) {
return;
}
$results = array(
OrdersStatsDataStore::sync_order( $order_id ),
ProductsDataStore::sync_order_products( $order_id ),
CouponsDataStore::sync_order_coupons( $order_id ),
TaxesDataStore::sync_order_taxes( $order_id ),
CustomersDataStore::sync_order_customer( $order_id ),
);
if ( 'shop_order' === $type ) {
$order_refunds = $order->get_refunds();
foreach ( $order_refunds as $refund ) {
OrdersStatsDataStore::sync_order( $refund->get_id() );
}
}
ReportsCache::invalidate();
}
/**
* Delete a batch of orders.
*
* @internal
* @param int $batch_size Number of items to delete.
* @return void
*/
public static function delete( $batch_size ) {
global $wpdb;
$order_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT order_id FROM {$wpdb->prefix}wc_order_stats ORDER BY order_id ASC LIMIT %d",
$batch_size
)
);
foreach ( $order_ids as $order_id ) {
OrdersStatsDataStore::delete_order( $order_id );
}
}
}
Schedulers/CustomersScheduler.php 0000644 00000007055 15073235522 0013214 0 ustar 00 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 );
}
}
}
Schedulers/ImportScheduler.php 0000644 00000011457 15073235522 0012503 0 ustar 00 search(
array(
'status' => 'pending',
'per_page' => 1,
'claimed' => false,
'search' => 'import',
'group' => self::$group,
)
);
if ( empty( $pending_jobs ) ) {
$in_progress = self::queue()->search(
array(
'status' => 'in-progress',
'per_page' => 1,
'search' => 'import',
'group' => self::$group,
)
);
}
return ! empty( $pending_jobs ) || ! empty( $in_progress );
}
/**
* Get batch sizes.
*
* @internal
* @retun array
*/
public static function get_batch_sizes() {
return array_merge(
self::get_scheduler_batch_sizes(),
array(
'delete' => 10,
'import' => 25,
'queue' => 100,
)
);
}
/**
* Get all available scheduling actions.
* Used to determine action hook names and clear events.
*
* @internal
* @return array
*/
public static function get_scheduler_actions() {
return array(
'import_batch_init' => 'wc-admin_import_batch_init_' . static::$name,
'import_batch' => 'wc-admin_import_batch_' . static::$name,
'delete_batch_init' => 'wc-admin_delete_batch_init_' . static::$name,
'delete_batch' => 'wc-admin_delete_batch_' . static::$name,
'import' => 'wc-admin_import_' . static::$name,
);
}
/**
* Queue the imports into multiple batches.
*
* @internal
* @param integer|boolean $days Number of days to import.
* @param boolean $skip_existing Skip exisiting records.
*/
public static function import_batch_init( $days, $skip_existing ) {
$batch_size = static::get_batch_size( 'import' );
$items = static::get_items( 1, 1, $days, $skip_existing );
if ( 0 === $items->total ) {
return;
}
$num_batches = ceil( $items->total / $batch_size );
self::queue_batches( 1, $num_batches, 'import_batch', array( $days, $skip_existing ) );
}
/**
* Imports a batch of items to update.
*
* @internal
* @param int $batch_number Batch number to import (essentially a query page number).
* @param int|bool $days Number of days to import.
* @param bool $skip_existing Skip exisiting records.
* @return void
*/
public static function import_batch( $batch_number, $days, $skip_existing ) {
$batch_size = static::get_batch_size( 'import' );
$properties = array(
'batch_number' => $batch_number,
'batch_size' => $batch_size,
'type' => static::$name,
);
wc_admin_record_tracks_event( 'import_job_start', $properties );
// When we are skipping already imported items, the table of items to import gets smaller in
// every batch, so we want to always import the first page.
$page = $skip_existing ? 1 : $batch_number;
$items = static::get_items( $batch_size, $page, $days, $skip_existing );
foreach ( $items->ids as $id ) {
static::import( $id );
}
$import_stats = get_option( self::IMPORT_STATS_OPTION, array() );
$imported_count = absint( $import_stats[ static::$name ]['imported'] ) + count( $items->ids );
$import_stats[ static::$name ]['imported'] = $imported_count;
update_option( self::IMPORT_STATS_OPTION, $import_stats );
$properties['imported_count'] = $imported_count;
wc_admin_record_tracks_event( 'import_job_complete', $properties );
}
/**
* Queue item deletion in batches.
*
* @internal
*/
public static function delete_batch_init() {
global $wpdb;
$batch_size = static::get_batch_size( 'delete' );
$count = static::get_total_imported();
if ( 0 === $count ) {
return;
}
$num_batches = ceil( $count / $batch_size );
self::queue_batches( 1, $num_batches, 'delete_batch' );
}
/**
* Delete a batch by passing the count to be deleted to the child delete method.
*
* @internal
* @return void
*/
public static function delete_batch() {
wc_admin_record_tracks_event( 'delete_import_data_job_start', array( 'type' => static::$name ) );
$batch_size = static::get_batch_size( 'delete' );
static::delete( $batch_size );
ReportsCache::invalidate();
wc_admin_record_tracks_event( 'delete_import_data_job_complete', array( 'type' => static::$name ) );
}
}
Schedulers/ImportInterface.php 0000644 00000001306 15073235522 0012455 0 ustar 00 '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' ),
'',
''
),
'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 Facebook for WooCommerce', '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' ),
'',
''
),
'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' ),
'',
''
),
'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' ),
'',
''
),
'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' ),
'',
''
),
'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' ),
'',
''
),
'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' ),
'',
''
),
'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://woo.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://woo.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://woo.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://woo.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://woo.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://woo.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://woo.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://woo.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;
}
}
RemoteFreeExtensions/Init.php 0000644 00000004225 15073235522 0012304 0 ustar 00 key, $allowed_bundles, true ) ) {
continue;
}
foreach ( $spec->plugins as $plugin ) {
try {
$extension = EvaluateExtension::evaluate( (object) $plugin );
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
$bundle['plugins'][] = $extension;
}
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch ( \Throwable $e ) {
// Ignore errors.
}
}
$bundles[] = $bundle;
}
return $bundles;
}
/**
* 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;
}
}
RemoteFreeExtensions/EvaluateExtension.php 0000644 00000003136 15073235522 0015044 0 ustar 00 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;
}
}
RemoteFreeExtensions/RemoteFreeExtensionsDataSourcePoller.php 0000644 00000001371 15073235522 0020646 0 ustar 00 'key',
)
);
}
return self::$instance;
}
}
ProductForm/ComponentTrait.php 0000644 00000001315 15073235522 0012473 0 ustar 00 id;
}
/**
* Return plugin id.
*
* @return string
*/
public function get_plugin_id() {
return $this->plugin_id;
}
}
ProductForm/FormFactory.php 0000644 00000016476 15073235522 0011776 0 ustar 00 (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()
);
}
}
}
ProductForm/Section.php 0000644 00000002234 15073235522 0011132 0 ustar 00 (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 )
)
);
}
}
}
ProductForm/Subsection.php 0000644 00000000304 15073235522 0011640 0 ustar 00 (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 )
)
);
}
}
}
ProductForm/Tab.php 0000644 00000002322 15073235522 0010232 0 ustar 00 (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 )
)
);
}
}
}
ProductForm/Component.php 0000644 00000005547 15073235522 0011502 0 ustar 00 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 = count( $path_keys );
$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 );
}
)
);
}
}
Events.php 0000644 00000021552 15073235522 0006532 0 ustar 00 possibly_add_notes();
$this->possibly_delete_notes();
$this->possibly_update_notes();
$this->possibly_refresh_data_source_pollers();
if ( $this->is_remote_inbox_notifications_enabled() ) {
DataSourcePoller::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();
}
}
}
Survey.php 0000644 00000001400 15073235522 0006551 0 ustar 00 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;
}
}
}
Loader.php 0000644 00000046365 15073235522 0006505 0 ustar 00 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 );
wc_get_container()->get( BlockTemplatesController::class );
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
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 );
?>
).
*
* @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 "
";
}
}
/**
* 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 '';
// Wrap the notices in a hidden div to prevent flickering before
// they are moved elsewhere in the page by WordPress Core.
echo '
';
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 '';
}
}
/**
* 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 '
';
}
/**
* 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() {
?>
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() );
}
}
ShippingLabelBanner.php 0000644 00000010226 15073235522 0011131 0 ustar 00 shipping_label_banner_display_rules ) {
$jetpack_version = null;
$jetpack_connected = null;
$wcs_version = null;
$wcs_tos_accepted = null;
if ( defined( 'JETPACK__VERSION' ) ) {
$jetpack_version = JETPACK__VERSION;
}
if ( class_exists( Jetpack_Connection_Manager::class ) ) {
$jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner();
}
if ( class_exists( '\WC_Connect_Loader' ) ) {
$wcs_version = \WC_Connect_Loader::get_wcs_version();
}
if ( class_exists( '\WC_Connect_Options' ) ) {
$wcs_tos_accepted = \WC_Connect_Options::get_option( 'tos_accepted' );
}
$incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) ||
class_exists( '\WC_Shipping_UPS_Init' ) ||
class_exists( '\WC_Integration_ShippingEasy' ) ||
class_exists( '\WC_ShipStation_Integration' );
$this->shipping_label_banner_display_rules =
new ShippingLabelBannerDisplayRules(
$jetpack_version,
$jetpack_connected,
$wcs_version,
$wcs_tos_accepted,
$incompatible_plugins
);
}
return $this->shipping_label_banner_display_rules->should_display_banner();
}
/**
* Add metabox to order page.
*
* @param string $post_type current post type.
* @param \WP_Post $post Current post object.
*/
public function add_meta_boxes( $post_type, $post ) {
if ( 'shop_order' !== $post_type ) {
return;
}
$order = wc_get_order( $post );
if ( $this->should_show_meta_box() ) {
add_meta_box(
'woocommerce-admin-print-label',
__( 'Shipping Label', 'woocommerce' ),
array( $this, 'meta_box' ),
null,
'normal',
'high',
array(
'context' => 'shipping_label',
'order' => $post->ID,
'items' => $this->count_shippable_items( $order ),
)
);
add_action( 'admin_enqueue_scripts', array( $this, 'add_print_shipping_label_script' ) );
}
}
/**
* Count shippable items
*
* @param \WC_Order $order Current order.
* @return int
*/
private function count_shippable_items( \WC_Order $order ) {
$count = 0;
foreach ( $order->get_items() as $item ) {
if ( $item instanceof \WC_Order_Item_Product ) {
$product = $item->get_product();
if ( $product && $product->needs_shipping() ) {
$count += $item->get_quantity();
}
}
}
return $count;
}
/**
* Adds JS to order page to render shipping banner.
*
* @param string $hook current page hook.
*/
public function add_print_shipping_label_script( $hook ) {
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style(
'print-shipping-label-banner-style',
WCAdminAssets::get_url( "print-shipping-label-banner/style{$rtl}", 'css' ),
array( 'wp-components' ),
WCAdminAssets::get_file_version( 'css' )
);
WCAdminAssets::register_script( 'wp-admin-scripts', 'print-shipping-label-banner', true );
$payload = array(
'nonce' => wp_create_nonce( 'wp_rest' ),
'baseURL' => get_rest_url(),
'wcs_server_connection' => true,
);
wp_localize_script( 'print-shipping-label-banner', 'wcConnectData', $payload );
}
/**
* Render placeholder metabox.
*
* @param \WP_Post $post current post.
* @param array $args empty args.
*/
public function meta_box( $post, $args ) {
?>
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'] = '
'
);
}
$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;
}
}
Onboarding/OnboardingProfile.php 0000644 00000003564 15073235522 0012756 0 ustar 00 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;
}
}
Onboarding/OnboardingMailchimp.php 0000644 00000002301 15073235522 0013245 0 ustar 00 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;
}
}
Onboarding/OnboardingSync.php 0000644 00000007701 15073235522 0012267 0 ustar 00 '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( $_SERVER['HTTP_REFERER'], 'https://woo.com/checkout?utm_medium=product' ) // phpcs:ignore sanitization ok.
) {
return;
}
wp_safe_redirect( wc_admin_url() );
}
}
Onboarding/OnboardingThemes.php 0000644 00000015656 15073235522 0012610 0 ustar 00 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 Woo.com
*
* @param array $themes Array of themes from Woo.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 );
}
}
Onboarding/Onboarding.php 0000644 00000001073 15073235522 0011426 0 ustar 00 init();
OnboardingIndustries::init();
OnboardingJetpack::instance()->init();
OnboardingMailchimp::instance()->init();
OnboardingProfile::init();
OnboardingSetupWizard::instance()->init();
OnboardingSync::instance()->init();
OnboardingThemes::init();
}
}
Onboarding/OnboardingProducts.php 0000644 00000012534 15073235522 0013156 0 ustar 00 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,
);
}
}
Onboarding/OnboardingJetpack.php 0000644 00000003464 15073235522 0012736 0 ustar 00 is_readable( $json_filename ) ) {
continue;
}
$file_contents = $wp_filesystem->get_contents( $json_filename );
$chunk_data = \json_decode( $file_contents, true );
if ( empty( $chunk_data ) ) {
continue;
}
if ( ! isset( $chunk_data['comment']['reference'] ) ) {
continue;
}
$reference_file = $chunk_data['comment']['reference'];
// Only combine "app" files (not scripts registered with WP).
if (
false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'app/index.js' ) &&
false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'chunks/' )
) {
continue;
}
if ( empty( $combined_translation_data ) ) {
// Use the first translation file as the base structure.
$combined_translation_data = $chunk_data;
} else {
// Combine all messages from all chunk files.
$combined_translation_data['locale_data']['messages'] = array_merge(
$combined_translation_data['locale_data']['messages'],
$chunk_data['locale_data']['messages']
);
}
}
// Remove inaccurate reference comment.
unset( $combined_translation_data['comment'] );
return $combined_translation_data;
}
/**
* Combines data from translation chunk files based on user-generated file formats,
* such as wp-cli tool or Loco Translate plugin.
*
* @param array $json_i18n_filenames List of JSON chunk files.
* @return array Combined translation chunk data.
*/
private function combine_user_translation_chunks( $json_i18n_filenames ) {
// the filesystem object should be hooked up.
global $wp_filesystem;
$combined_translation_data = array();
foreach ( $json_i18n_filenames as $json_filename ) {
if ( ! $wp_filesystem->is_readable( $json_filename ) ) {
continue;
}
$file_contents = $wp_filesystem->get_contents( $json_filename );
$chunk_data = \json_decode( $file_contents, true );
if ( empty( $chunk_data ) ) {
continue;
}
$reference_file = $chunk_data['source'];
// Only combine "app" files (not scripts registered with WP).
if (
false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'app/index.js' ) &&
false === strpos( $reference_file, WC_ADMIN_DIST_JS_FOLDER . 'chunks/' )
) {
continue;
}
if ( empty( $combined_translation_data ) ) {
// Use the first translation file as the base structure.
$combined_translation_data = $chunk_data;
} else {
// Combine all messages from all chunk files.
$combined_translation_data['locale_data']['woocommerce'] = array_merge(
$combined_translation_data['locale_data']['woocommerce'],
$chunk_data['locale_data']['woocommerce']
);
}
}
// Remove inaccurate reference comment.
unset( $combined_translation_data['source'] );
return $combined_translation_data;
}
/**
* Find and combine translation chunk files.
*
* Only targets files that aren't represented by a registered script (e.g. not passed to wp_register_script()).
*
* @param string $lang_dir Path to language files.
* @param string $domain Text domain.
* @param string $locale Locale being retrieved.
* @return array Combined translation chunk data.
*/
private function get_translation_chunk_data( $lang_dir, $domain, $locale ) {
// So long as this function is called during the 'upgrader_process_complete' action,
// the filesystem object should be hooked up.
global $wp_filesystem;
// Grab all JSON files in the current language pack.
$json_i18n_filenames = glob( $lang_dir . $domain . '-' . $locale . '-*.json' );
$combined_translation_data = array();
if ( false === $json_i18n_filenames ) {
return $combined_translation_data;
}
// Use first JSON file to determine file format. This check is required due to
// file format difference between official language files and user translated files.
$format_determine_file = reset( $json_i18n_filenames );
if ( ! $wp_filesystem->is_readable( $format_determine_file ) ) {
return $combined_translation_data;
}
$file_contents = $wp_filesystem->get_contents( $format_determine_file );
$format_determine_data = \json_decode( $file_contents, true );
if ( empty( $format_determine_data ) ) {
return $combined_translation_data;
}
if ( isset( $format_determine_data['comment'] ) ) {
return $this->combine_official_translation_chunks( $json_i18n_filenames );
} elseif ( isset( $format_determine_data['source'] ) ) {
return $this->combine_user_translation_chunks( $json_i18n_filenames );
} else {
return $combined_translation_data;
}
}
/**
* Combine and save translations for a specific locale.
*
* Note that this assumes \WP_Filesystem is already initialized with write access.
*
* @param string $language_dir Path to language files.
* @param string $plugin_domain Text domain.
* @param string $locale Locale being retrieved.
*/
private function build_and_save_translations( $language_dir, $plugin_domain, $locale ) {
global $wp_filesystem;
$translations_from_chunks = $this->get_translation_chunk_data( $language_dir, $plugin_domain, $locale );
if ( empty( $translations_from_chunks ) ) {
return;
}
$cache_filename = $this->get_combined_translation_filename( $plugin_domain, $locale );
$chunk_translations_json = wp_json_encode( $translations_from_chunks );
// Cache combined translations strings to a file.
$wp_filesystem->put_contents( $language_dir . $cache_filename, $chunk_translations_json );
}
/**
* Combine translation chunks when plugin is activated.
*
* This function combines JSON translation data auto-extracted by GlotPress
* from Webpack-generated JS chunks into a single file. This is necessary
* since the JS chunks are not known to WordPress via wp_register_script()
* and wp_set_script_translations().
*/
private function generate_translation_strings() {
$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
$locale = determine_locale();
$lang_dir = WP_LANG_DIR . '/plugins/';
// Bail early if not localized.
if ( 'en_US' === $locale ) {
return;
}
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$access_type = get_filesystem_method();
if ( 'direct' === $access_type ) {
\WP_Filesystem();
$this->build_and_save_translations( $lang_dir, $plugin_domain, $locale );
} else {
// I'm reluctant to add support for other filesystems here as it would require
// user's input on activating plugin - which I don't think is common.
return;
}
}
/**
* Loads the required translation scripts on the correct pages.
*/
public function potentially_load_translation_script_file() {
if ( ! PageController::is_admin_or_embed_page() ) {
return;
}
// Grab translation strings from Webpack-generated chunks.
add_filter( 'load_script_translation_file', array( $this, 'load_script_translation_file' ), 10, 3 );
}
/**
* Load translation strings from language packs for dynamic imports.
*
* @param string $file File location for the script being translated.
* @param string $handle Script handle.
* @param string $domain Text domain.
*
* @return string New file location for the script being translated.
*/
public function load_script_translation_file( $file, $handle, $domain ) {
// Make sure the main app script is being loaded.
if ( WC_ADMIN_APP !== $handle ) {
return $file;
}
// Make sure we're handing the correct domain (could be woocommerce or woocommerce-admin).
$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
if ( $plugin_domain !== $domain ) {
return $file;
}
$locale = determine_locale();
$cache_filename = $this->get_combined_translation_filename( $domain, $locale );
return WP_LANG_DIR . '/plugins/' . $cache_filename;
}
/**
* Run when plugin is activated (can be WooCommerce or WooCommerce Admin).
*
* @param string $filename Activated plugin filename.
*/
public function potentially_generate_translation_strings( $filename ) {
$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
$activated_plugin_domain = explode( '/', $filename )[0];
// Ensure we're only running only on activation hook that originates from our plugin.
if ( $plugin_domain === $activated_plugin_domain ) {
$this->generate_translation_strings();
}
}
/**
* Combine translation chunks when files are updated.
*
* This function combines JSON translation data auto-extracted by GlotPress
* from Webpack-generated JS chunks into a single file that can be used in
* subsequent requests. This is necessary since the JS chunks are not known
* to WordPress via wp_register_script() and wp_set_script_translations().
*
* @param Language_Pack_Upgrader $instance Upgrader instance.
* @param array $hook_extra Info about the upgraded language packs.
*/
public function combine_translation_chunk_files( $instance, $hook_extra ) {
if (
! is_a( $instance, 'Language_Pack_Upgrader' ) ||
! isset( $hook_extra['translations'] ) ||
! is_array( $hook_extra['translations'] )
) {
return;
}
// Make sure we're handing the correct domain (could be woocommerce or woocommerce-admin).
$plugin_domain = explode( '/', plugin_basename( __FILE__ ) )[0];
$locales = array();
$language_dir = WP_LANG_DIR . '/plugins/';
// Gather the locales that were updated in this operation.
foreach ( $hook_extra['translations'] as $translation ) {
if (
'plugin' === $translation['type'] &&
$plugin_domain === $translation['slug']
) {
$locales[] = $translation['language'];
}
}
// Build combined translation files for all updated locales.
foreach ( $locales as $locale ) {
// So long as this function is hooked to the 'upgrader_process_complete' action,
// WP_Filesystem should be hooked up to be able to call build_and_save_translations.
$this->build_and_save_translations( $language_dir, $plugin_domain, $locale );
}
}
}
FeaturePlugin.php 0000644 00000015012 15073235522 0010032 0 ustar 00 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();
}
}
Orders/COTRedirectionController.php 0000644 00000005405 15073235522 0013404 0 ustar 00 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;
}
}
}
Orders/MetaBoxes/CustomMetaBox.php 0000644 00000036731 15073235522 0013152 0 ustar 00 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 ) {
?>
render_meta_form( $order );
?>
',
''
);
?>
order_meta_keys_autofill( null, $order );
/**
* 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', $keys, $order );
?>
';
}
/**
* Renders advice in the event that no orders exist yet.
*
* @return void
*/
public function render_blank_state(): void {
?>
order_query_args['status'] ?? false;
if ( array( 'trash' ) === $selected_status ) {
$actions = array(
'untrash' => __( 'Restore', 'woocommerce' ),
'delete' => __( 'Delete permanently', 'woocommerce' ),
);
} else {
$actions = array(
'mark_processing' => __( 'Change status to processing', 'woocommerce' ),
'mark_on-hold' => __( 'Change status to on-hold', 'woocommerce' ),
'mark_completed' => __( 'Change status to completed', 'woocommerce' ),
'mark_cancelled' => __( 'Change status to cancelled', 'woocommerce' ),
'trash' => __( 'Move to Trash', 'woocommerce' ),
);
}
if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) {
$actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' );
}
return $actions;
}
/**
* Gets a list of CSS classes for the WP_List_Table table tag.
*
* @since 7.8.0
*
* @return string[] Array of CSS classes for the table tag.
*/
protected function get_table_classes() {
/**
* Filters the list of CSS class names for the orders list table.
*
* @since 7.8.0
*
* @param string[] $classes An array of CSS class names.
* @param string $order_type The order type.
*/
$css_classes = apply_filters(
'woocommerce_' . $this->order_type . '_list_table_css_classes',
array_merge(
parent::get_table_classes(),
array(
'wc-orders-list-table',
'wc-orders-list-table-' . $this->order_type,
)
),
$this->order_type
);
return array_unique( array_map( 'trim', $css_classes ) );
}
/**
* Prepares the list of items for displaying.
*/
public function prepare_items() {
$limit = $this->get_items_per_page( 'edit_' . $this->order_type . '_per_page' );
$this->order_query_args = array(
'limit' => $limit,
'page' => $this->get_pagenum(),
'paginate' => true,
'type' => $this->order_type,
);
foreach ( array( 'status', 's', 'm', '_customer_user' ) as $query_var ) {
$this->request[ $query_var ] = sanitize_text_field( wp_unslash( $_REQUEST[ $query_var ] ?? '' ) );
}
/**
* Allows 3rd parties to filter the initial request vars before defaults and other logic is applied.
*
* @param array $request Request to be passed to `wc_get_orders()`.
*
* @since 7.3.0
*/
$this->request = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_request', $this->request );
$this->set_status_args();
$this->set_order_args();
$this->set_date_args();
$this->set_customer_args();
$this->set_search_args();
/**
* Provides an opportunity to modify the query arguments used in the (Custom Order Table-powered) order list
* table.
*
* @since 6.9.0
*
* @param array $query_args Arguments to be passed to `wc_get_orders()`.
*/
$order_query_args = (array) apply_filters( 'woocommerce_order_list_table_prepare_items_query_args', $this->order_query_args );
/**
* Same as `woocommerce_order_list_table_prepare_items_query_args` but for a specific order type.
*
* @param array $query_args Arguments to be passed to `wc_get_orders()`.
*
* @since 7.3.0
*/
$order_query_args = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_prepare_items_query_args', $order_query_args );
// We must ensure the 'paginate' argument is set.
$order_query_args['paginate'] = true;
$orders = wc_get_orders( $order_query_args );
$this->items = $orders->orders;
$max_num_pages = $orders->max_num_pages;
// Check in case the user has attempted to page beyond the available range of orders.
if ( 0 === $max_num_pages && $this->order_query_args['page'] > 1 ) {
$count_query_args = $order_query_args;
$count_query_args['page'] = 1;
$count_query_args['limit'] = 1;
$order_count = wc_get_orders( $count_query_args );
$max_num_pages = (int) ceil( $order_count->total / $order_query_args['limit'] );
}
$this->set_pagination_args(
array(
'total_items' => $orders->total ?? 0,
'per_page' => $limit,
'total_pages' => $max_num_pages,
)
);
// Are we inside the trash?
$this->is_trash = 'trash' === $this->request['status'];
}
/**
* Updates the WC Order Query arguments as needed to support orderable columns.
*/
private function set_order_args() {
$sortable = $this->get_sortable_columns();
$field = sanitize_text_field( wp_unslash( $_GET['orderby'] ?? '' ) );
$direction = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ?? '' ) ) );
if ( ! in_array( $field, $sortable, true ) ) {
$this->order_query_args['orderby'] = 'date';
$this->order_query_args['order'] = 'DESC';
return;
}
$this->order_query_args['orderby'] = $field;
$this->order_query_args['order'] = in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC';
}
/**
* Implements date (month-based) filtering.
*/
private function set_date_args() {
$year_month = sanitize_text_field( wp_unslash( $_GET['m'] ?? '' ) );
if ( empty( $year_month ) || ! preg_match( '/^[0-9]{6}$/', $year_month ) ) {
return;
}
$year = (int) substr( $year_month, 0, 4 );
$month = (int) substr( $year_month, 4, 2 );
if ( $month < 0 || $month > 12 ) {
return;
}
$last_day_of_month = date_create( "$year-$month" )->format( 'Y-m-t' );
$this->order_query_args['date_created'] = "$year-$month-01..." . $last_day_of_month;
$this->has_filter = true;
}
/**
* Implements filtering of orders by customer.
*/
private function set_customer_args() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$customer = (int) wp_unslash( $_GET['_customer_user'] ?? '' );
if ( $customer < 1 ) {
return;
}
$this->order_query_args['customer'] = $customer;
$this->has_filter = true;
}
/**
* Implements filtering of orders by status.
*/
private function set_status_args() {
$status = array_filter( array_map( 'trim', (array) $this->request['status'] ) );
if ( empty( $status ) || in_array( 'all', $status, true ) ) {
/**
* Allows 3rd parties to set the default list of statuses for a given order type.
*
* @param string[] $statuses Statuses.
*
* @since 7.3.0
*/
$status = apply_filters(
'woocommerce_' . $this->order_type . '_list_table_default_statuses',
array_intersect(
array_keys( wc_get_order_statuses() ),
get_post_stati( array( 'show_in_admin_all_list' => true ), 'names' )
)
);
} else {
$this->has_filter = true;
}
$this->order_query_args['status'] = $status;
}
/**
* Implements order search.
*/
private function set_search_args(): void {
$search_term = trim( sanitize_text_field( $this->request['s'] ) );
if ( ! empty( $search_term ) ) {
$this->order_query_args['s'] = $search_term;
$this->has_filter = true;
}
}
/**
* Get the list of views for this table (all orders, completed orders, etc, each with a count of the number of
* corresponding orders).
*
* @return array
*/
public function get_views() {
$view_counts = array();
$view_links = array();
$statuses = $this->get_visible_statuses();
$current = ! empty( $this->request['status'] ) ? sanitize_text_field( $this->request['status'] ) : 'all';
$all_count = 0;
foreach ( array_keys( $statuses ) as $slug ) {
$total_in_status = $this->count_orders_by_status( $slug );
if ( $total_in_status > 0 ) {
$view_counts[ $slug ] = $total_in_status;
}
if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list && 'auto-draft' !== $slug ) {
$all_count += $total_in_status;
}
}
$view_links['all'] = $this->get_view_link( 'all', __( 'All', 'woocommerce' ), $all_count, '' === $current || 'all' === $current );
foreach ( $view_counts as $slug => $count ) {
$view_links[ $slug ] = $this->get_view_link( $slug, $statuses[ $slug ], $count, $slug === $current );
}
return $view_links;
}
/**
* Count orders by status.
*
* @param string|string[] $status The order status we are interested in.
*
* @return int
*/
private function count_orders_by_status( $status ): int {
global $wpdb;
// Compute all counts and cache if necessary.
if ( is_null( $this->status_count_cache ) ) {
$orders_table = OrdersTableDataStore::get_orders_table_name();
$res = $wpdb->get_results(
$wpdb->prepare(
"SELECT status, COUNT(*) AS cnt FROM {$orders_table} WHERE type = %s GROUP BY status", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->order_type
),
ARRAY_A
);
$this->status_count_cache =
$res
? array_combine( array_column( $res, 'status' ), array_map( 'absint', array_column( $res, 'cnt' ) ) )
: array();
}
$status = (array) $status;
$count = array_sum( array_intersect_key( $this->status_count_cache, array_flip( $status ) ) );
/**
* Allows 3rd parties to modify the count of orders by status.
*
* @param int $count Number of orders for the given status.
* @param string[] $status List of order statuses in the count.
* @since 7.3.0
*/
return apply_filters(
'woocommerce_' . $this->order_type . '_list_table_order_count',
$count,
$status
);
}
/**
* Checks whether the blank state should be rendered or not. This depends on whether there are others with a visible
* status.
*
* @return boolean TRUE when the blank state should be rendered, FALSE otherwise.
*/
private function should_render_blank_state(): bool {
return ( ! $this->has_filter ) && 0 === $this->count_orders_by_status( array_keys( $this->get_visible_statuses() ) );
}
/**
* Returns a list of slug and labels for order statuses that should be visible in the status list.
*
* @return array slug => label array of order statuses.
*/
private function get_visible_statuses(): array {
return array_intersect_key(
array_merge(
wc_get_order_statuses(),
array(
'trash' => ( get_post_status_object( 'trash' ) )->label,
'draft' => ( get_post_status_object( 'draft' ) )->label,
'auto-draft' => ( get_post_status_object( 'auto-draft' ) )->label,
)
),
array_flip( get_post_stati( array( 'show_in_admin_status_list' => true ) ) )
);
}
/**
* Form a link to use in the list of table views.
*
* @param string $slug Slug used to identify the view (usually the order status slug).
* @param string $name Human-readable name of the view (usually the order status label).
* @param int $count Number of items in this view.
* @param bool $current If this is the current view.
*
* @return string
*/
private function get_view_link( string $slug, string $name, int $count, bool $current ): string {
$base_url = get_admin_url( null, 'admin.php?page=wc-orders' . ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type ) );
$url = esc_url( add_query_arg( 'status', $slug, $base_url ) );
$name = esc_html( $name );
$count = absint( $count );
$class = $current ? 'class="current"' : '';
return "$name ($count)";
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @param string $which Either 'top' or 'bottom'.
*/
protected function extra_tablenav( $which ) {
echo '
';
if ( 'top' === $which ) {
ob_start();
$this->months_filter();
/**
* Fires before the "Filter" button on the list table for orders and other order types.
*
* @since 7.3.0
*
* @param string $order_type The order type.
* @param string $which The location of the extra table nav: 'top' or 'bottom'.
*/
do_action( 'woocommerce_order_list_table_restrict_manage_orders', $this->order_type, $which );
$output = ob_get_clean();
if ( ! empty( $output ) ) {
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'order-query-submit' ) );
}
}
if ( $this->is_trash && $this->has_items() && current_user_can( 'edit_others_shop_orders' ) ) {
submit_button( __( 'Empty Trash', 'woocommerce' ), 'apply', 'delete_all', false );
}
/**
* Fires immediately following the closing "actions" div in the tablenav for the order
* list table.
*
* @since 7.3.0
*
* @param string $order_type The order type.
* @param string $which The location of the extra table nav: 'top' or 'bottom'.
*/
do_action( 'woocommerce_order_list_table_extra_tablenav', $this->order_type, $which );
echo '
';
}
/**
* Render the months filter dropdown.
*
* @return void
*/
private function months_filter() {
// XXX: [review] we may prefer to move this logic outside of the ListTable class.
global $wp_locale;
global $wpdb;
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
$utc_offset = wc_timezone_offset();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$order_dates = $wpdb->get_results(
"
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
MONTH( t.date_created_local ) AS month
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE status != 'trash' ) t
ORDER BY year DESC, month DESC
"
);
$m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
echo '';
}
/**
* Render the customer filter dropdown.
*
* @return void
*/
public function customers_filter() {
$user_string = '';
$user_id = '';
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['_customer_user'] ) ) {
$user_id = absint( $_GET['_customer_user'] );
$user = get_user_by( 'id', $user_id );
$user_string = sprintf(
/* translators: 1: user display name 2: user ID 3: user email */
esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ),
$user->display_name,
absint( $user->ID ),
$user->user_email
);
}
// Note: use of htmlspecialchars (below) is to prevent XSS when rendered by selectWoo.
?>
order_type . '_list_table_columns',
array(
'cb' => '',
'order_number' => esc_html__( 'Order', 'woocommerce' ),
'order_date' => esc_html__( 'Date', 'woocommerce' ),
'order_status' => esc_html__( 'Status', 'woocommerce' ),
'billing_address' => esc_html__( 'Billing', 'woocommerce' ),
'shipping_address' => esc_html__( 'Ship to', 'woocommerce' ),
'order_total' => esc_html__( 'Total', 'woocommerce' ),
'wc_actions' => esc_html__( 'Actions', 'woocommerce' ),
)
);
}
/**
* Defines the default sortable columns.
*
* @return string[]
*/
public function get_sortable_columns() {
/**
* Filters the list of sortable columns.
*
* @param array $sortable_columns List of sortable columns.
*
* @since 7.3.0
*/
return apply_filters(
'woocommerce_' . $this->order_type . '_list_table_sortable_columns',
array(
'order_number' => 'ID',
'order_date' => 'date',
'order_total' => 'order_total',
)
);
}
/**
* Specify the columns we wish to hide by default.
*
* @param array $hidden Columns set to be hidden.
* @param WP_Screen $screen Screen object.
*
* @return array
*/
public function default_hidden_columns( array $hidden, WP_Screen $screen ) {
if ( isset( $screen->id ) && wc_get_page_screen_id( 'shop-order' ) === $screen->id ) {
$hidden = array_merge(
$hidden,
array(
'billing_address',
'shipping_address',
'wc_actions',
)
);
}
return $hidden;
}
/**
* Checklist column, used for selecting items for processing by a bulk action.
*
* @param WC_Order $item The order object for the current row.
*
* @return string
*/
public function column_cb( $item ) {
ob_start();
?>
get_id() ) );
?>
get_billing_first_name() || $order->get_billing_last_name() ) {
/* translators: 1: first name 2: last name */
$buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) );
} elseif ( $order->get_billing_company() ) {
$buyer = trim( $order->get_billing_company() );
} elseif ( $order->get_customer_id() ) {
$user = get_user_by( 'id', $order->get_customer_id() );
$buyer = ucwords( $user->display_name );
}
/**
* Filter buyer name in list table orders.
*
* @since 3.7.0
*
* @param string $buyer Buyer name.
* @param WC_Order $order Order data.
*/
$buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $order );
if ( $order->get_status() === 'trash' ) {
echo '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '';
} else {
echo '' . esc_html( __( 'Preview', 'woocommerce' ) ) . '';
echo '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '';
}
}
/**
* Get the edit link for an order.
*
* @param WC_Order $order Order object.
*
* @return string Edit link for the order.
*/
private function get_order_edit_link( WC_Order $order ) : string {
return $this->page_controller->get_edit_url( $order->get_id() );
}
/**
* Renders the order date.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_order_date_column( WC_Order $order ): void {
$order_timestamp = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : '';
if ( ! $order_timestamp ) {
echo '–';
return;
}
// Check if the order was created within the last 24 hours, and not in the future.
if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) {
$show_date = sprintf(
/* translators: %s: human-readable time difference */
_x( '%s ago', '%s = human-readable time difference', 'woocommerce' ),
human_time_diff( $order->get_date_created()->getTimestamp(), time() )
);
} else {
$show_date = $order->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
printf(
'',
esc_attr( $order->get_date_created()->date( 'c' ) ),
esc_html( $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
esc_html( $show_date )
);
}
/**
* Renders the order status.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_order_status_column( WC_Order $order ): void {
$tooltip = '';
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$comment_count = get_comment_count( $order->get_id() );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$approved_comments_count = absint( $comment_count['approved'] );
if ( $approved_comments_count ) {
$latest_notes = wc_get_order_notes(
array(
'order_id' => $order->get_id(),
'limit' => 1,
'orderby' => 'date_created_gmt',
)
);
$latest_note = current( $latest_notes );
if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) {
$tooltip = wc_sanitize_tooltip( $latest_note->content );
} elseif ( isset( $latest_note->content ) ) {
/* translators: %d: notes count */
$tooltip = wc_sanitize_tooltip( $latest_note->content . ' ' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '' );
} else {
/* translators: %d: notes count */
$tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) );
}
}
// Gracefully handle legacy statuses.
if ( in_array( $order->get_status(), array( 'trash', 'draft', 'auto-draft' ), true ) ) {
$status_name = ( get_post_status_object( $order->get_status() ) )->label;
} else {
$status_name = wc_get_order_status_name( $order->get_status() );
}
if ( $tooltip ) {
printf( '%s', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), wp_kses_post( $tooltip ), esc_html( $status_name ) );
} else {
printf( '%s', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), esc_html( $status_name ) );
}
}
/**
* Renders order billing information.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_billing_address_column( WC_Order $order ): void {
$address = $order->get_formatted_billing_address();
if ( $address ) {
echo esc_html( preg_replace( '# #i', ', ', $address ) );
if ( $order->get_payment_method() ) {
/* translators: %s: payment method */
echo '' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_payment_method_title() ) ) . '';
}
} else {
echo '–';
}
}
/**
* Renders order shipping information.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_shipping_address_column( WC_Order $order ): void {
$address = $order->get_formatted_shipping_address();
if ( $address ) {
echo '' . esc_html( preg_replace( '# #i', ', ', $address ) ) . '';
if ( $order->get_shipping_method() ) {
/* translators: %s: shipping method */
echo '' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_shipping_method() ) ) . '';
}
} else {
echo '–';
}
}
/**
* Renders the order total.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_order_total_column( WC_Order $order ): void {
if ( $order->get_payment_method_title() ) {
/* translators: %s: method */
echo '' . wp_kses_post( $order->get_formatted_order_total() ) . '';
} else {
echo wp_kses_post( $order->get_formatted_order_total() );
}
}
/**
* Renders order actions.
*
* @param WC_Order $order The order object for the current row.
*
* @return void
*/
public function render_wc_actions_column( WC_Order $order ): void {
echo '
';
/**
* Fires before the order action buttons (within the actions column for the order list table)
* are registered.
*
* @param WC_Order $order Current order object.
* @since 6.7.0
*/
do_action( 'woocommerce_admin_order_actions_start', $order );
$actions = array();
if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) {
$actions['processing'] = array(
'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
'name' => __( 'Processing', 'woocommerce' ),
'action' => 'processing',
);
}
if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) {
$actions['complete'] = array(
'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
'name' => __( 'Complete', 'woocommerce' ),
'action' => 'complete',
);
}
/**
* Provides an opportunity to modify the action buttons within the order list table.
*
* @param array $action Order actions.
* @param WC_Order $order Current order object.
* @since 6.7.0
*/
$actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $order );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wc_render_action_buttons( $actions );
/**
* Fires after the order action buttons (within the actions column for the order list table)
* are rendered.
*
* @param WC_Order $order Current order object.
* @since 6.7.0
*/
do_action( 'woocommerce_admin_order_actions_end', $order );
echo '
';
}
/**
* Outputs hidden fields used to retain state when filtering.
*
* @return void
*/
private function print_hidden_form_fields(): void {
echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$state_params = array(
'paged',
'status',
);
foreach ( $state_params as $param ) {
if ( ! isset( $_GET[ $param ] ) ) {
continue;
}
echo '';
}
}
/**
* Gets the current action selected from the bulk actions dropdown.
*
* @return string|false The action name. False if no action was selected.
*/
public function current_action() {
if ( ! empty( $_REQUEST['delete_all'] ) ) {
return 'delete_all';
}
return parent::current_action();
}
/**
* Handle bulk actions.
*/
public function handle_bulk_actions() {
$action = $this->current_action();
if ( ! $action ) {
return;
}
check_admin_referer( 'bulk-orders' );
$redirect_to = remove_query_arg( array( 'deleted', 'ids' ), wp_get_referer() );
$redirect_to = add_query_arg( 'paged', $this->get_pagenum(), $redirect_to );
if ( 'delete_all' === $action ) {
// Get all trashed orders.
$ids = wc_get_orders(
array(
'type' => $this->order_type,
'status' => 'trash',
'limit' => -1,
'return' => 'ids',
)
);
$action = 'delete';
} else {
$ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array();
}
/**
* Allows 3rd parties to modify order IDs about to be affected by a bulk action.
*
* @param array Array of order IDs.
*/
$ids = apply_filters( // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
'woocommerce_bulk_action_ids',
$ids,
$action,
'order'
);
if ( ! $ids ) {
wp_safe_redirect( $redirect_to );
exit;
}
$report_action = '';
$changed = 0;
$action_handled = true;
if ( 'remove_personal_data' === $action ) {
$report_action = 'removed_personal_data';
$changed = $this->do_bulk_action_remove_personal_data( $ids );
} elseif ( 'trash' === $action ) {
$changed = $this->do_delete( $ids );
$report_action = 'trashed';
} elseif ( 'delete' === $action ) {
$changed = $this->do_delete( $ids, true );
$report_action = 'deleted';
} elseif ( 'untrash' === $action ) {
$changed = $this->do_untrash( $ids );
$report_action = 'untrashed';
} elseif ( false !== strpos( $action, 'mark_' ) ) {
$order_statuses = wc_get_order_statuses();
$new_status = substr( $action, 5 );
$report_action = 'marked_' . $new_status;
if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) {
$changed = $this->do_bulk_action_mark_orders( $ids, $new_status );
} else {
$action_handled = false;
}
} else {
$action_handled = false;
}
// Custom action.
if ( ! $action_handled ) {
$screen = get_current_screen()->id;
/**
* This action is documented in /wp-admin/edit.php (it is a core WordPress hook).
*
* @since 7.2.0
*
* @param string $redirect_to The URL to redirect to after processing the bulk actions.
* @param string $action The current bulk action.
* @param int[] $ids IDs for the orders to be processed.
*/
$custom_sendback = apply_filters( "handle_bulk_actions-{$screen}", $redirect_to, $action, $ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}
if ( ! empty( $custom_sendback ) ) {
$redirect_to = $custom_sendback;
} elseif ( $changed ) {
$redirect_to = add_query_arg(
array(
'bulk_action' => $report_action,
'changed' => $changed,
'ids' => implode( ',', $ids ),
),
$redirect_to
);
}
wp_safe_redirect( $redirect_to );
exit;
}
/**
* Implements the "remove personal data" bulk action.
*
* @param array $order_ids The Order IDs.
* @return int Number of orders modified.
*/
private function do_bulk_action_remove_personal_data( $order_ids ): int {
$changed = 0;
foreach ( $order_ids as $id ) {
$order = wc_get_order( $id );
if ( ! $order ) {
continue;
}
do_action( 'woocommerce_remove_order_personal_data', $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$changed++;
}
return $changed;
}
/**
* Implements the "mark " bulk action.
*
* @param array $order_ids The order IDs to change.
* @param string $new_status The new order status.
* @return int Number of orders modified.
*/
private function do_bulk_action_mark_orders( $order_ids, $new_status ): int {
$changed = 0;
// Initialize payment gateways in case order has hooked status transition actions.
WC()->payment_gateways();
foreach ( $order_ids as $id ) {
$order = wc_get_order( $id );
if ( ! $order ) {
continue;
}
$order->update_status( $new_status, __( 'Order status changed by bulk edit.', 'woocommerce' ), true );
do_action( 'woocommerce_order_edit_status', $id, $new_status ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$changed++;
}
return $changed;
}
/**
* Handles bulk trashing of orders.
*
* @param int[] $ids Order IDs to be trashed.
* @param bool $force_delete When set, the order will be completed deleted. Otherwise, it will be trashed.
*
* @return int Number of orders that were trashed.
*/
private function do_delete( array $ids, bool $force_delete = false ): int {
$changed = 0;
foreach ( $ids as $id ) {
$order = wc_get_order( $id );
$order->delete( $force_delete );
$updated_order = wc_get_order( $id );
if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
$changed++;
}
}
return $changed;
}
/**
* Handles bulk restoration of trashed orders.
*
* @param array $ids Order IDs to be restored to their previous status.
*
* @return int Number of orders that were restored from the trash.
*/
private function do_untrash( array $ids ): int {
$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
$changed = 0;
foreach ( $ids as $id ) {
if ( $orders_store->untrash_order( wc_get_order( $id ) ) ) {
$changed++;
}
}
return $changed;
}
/**
* Show confirmation message that order status changed for number of orders.
*/
public function bulk_action_notices() {
if ( empty( $_REQUEST['bulk_action'] ) ) {
return;
}
$order_statuses = wc_get_order_statuses();
$number = absint( $_REQUEST['changed'] ?? 0 );
$bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) );
$message = '';
// Check if any status changes happened.
foreach ( $order_statuses as $slug => $name ) {
if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok.
/* translators: %s: orders count */
$message = sprintf( _n( '%s order status changed.', '%s order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) );
break;
}
}
switch ( $bulk_action ) {
case 'removed_personal_data':
/* translators: %s: orders count */
$message = sprintf( _n( 'Removed personal data from %s order.', 'Removed personal data from %s orders.', $number, 'woocommerce' ), number_format_i18n( $number ) );
echo '
' . esc_html( $message ) . '
';
break;
case 'trashed':
/* translators: %s: orders count */
$message = sprintf( _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) );
break;
case 'untrashed':
/* translators: %s: orders count */
$message = sprintf( _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) );
break;
case 'deleted':
/* translators: %s: orders count */
$message = sprintf( _n( '%s order permanently deleted.', '%s orders permanently deleted.', $number, 'woocommerce' ), number_format_i18n( $number ) );
break;
}
if ( ! empty( $message ) ) {
echo '
' . esc_html( $message ) . '
';
}
}
/**
* Enqueue list table scripts.
*
* @return void
*/
public function enqueue_scripts(): void {
echo $this->get_order_preview_template(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_enqueue_script( 'wc-orders' );
}
/**
* Returns the HTML for the order preview template.
*
* @return string HTML template.
*/
public function get_order_preview_template(): string {
$order_edit_url_placeholder =
wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
? esc_url( admin_url( 'admin.php?page=wc-orders&action=edit' ) ) . '&id={{ data.data.id }}'
: esc_url( admin_url( 'post.php?action=edit' ) ) . '&post={{ data.data.id }}';
ob_start();
?>
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_` 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'
);
/**
* 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() );
?>
',
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-', $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(
'%2$s',
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 = '' . $match[0] . '';
$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'];
}
}
Logging/FileV2/File.php 0000644 00000031755 15073235522 0010630 0 ustar 00 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' );
}
}
Logging/LogHandlerFileV2.php 0000644 00000014414 15073235522 0011702 0 ustar 00 file_controller = wc_get_container()->get( FileController::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 );
// Remove line breaks so the whole entry is on one line in the file.
$formatted_message = str_replace( PHP_EOL, ' ', $message );
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 );
$formatted_message .= " CONTEXT: $formatted_context";
}
$entry = "$time_string $level_string $formatted_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 ) {
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 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;
}
$file_ids = array_map(
fn( $file ) => $file->get_file_id(),
$files
);
$deleted = $this->file_controller->delete_files( $file_ids );
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
/** This filter is documented in includes/class-wc-logger.php. */
$retention_days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
if ( $deleted > 0 ) {
$this->handle(
time(),
'info',
sprintf(
'%s %s',
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 )
),
sprintf(
esc_html(
// translators: %s is a number of days.
_n(
'The retention period for log files is %s day.',
'The retention period for log files is %s days.',
$retention_days,
'woocommerce'
)
),
number_format_i18n( $retention_days )
)
),
array(
'source' => 'wc_logger',
)
);
}
return $deleted;
}
}
Logging/PageController.php 0000644 00000045411 15073235522 0011574 0 ustar 00 file_controller = $file_controller;
$this->init_hooks();
}
/**
* Add callbacks to hooks.
*
* @return void
*/
private function init_hooks(): void {
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'setup_screen_options' ) );
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'handle_list_table_bulk_actions' ) );
}
/**
* 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' )
);
}
/**
* Determine the default log handler.
*
* @return string
*/
public function get_default_handler(): string {
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
if ( is_null( $handler ) || ! class_exists( $handler ) ) {
$handler = WC_Log_Handler_File::class;
}
return $handler;
}
/**
* Render the "Logs" tab, depending on the current default log handler.
*
* @return void
*/
public function render(): void {
$handler = $this->get_default_handler();
switch ( $handler ) {
case LogHandlerFileV2::class:
$this->render_filev2();
break;
case 'WC_Log_Handler_DB':
WC_Admin_Status::status_logs_db();
break;
default:
WC_Admin_Status::status_logs_file();
break;
}
}
/**
* 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();
?>