File manager - Edit - /home/monara/public_html/test.athavaneng.com/Admin.tar
Back
SystemStatusReport.php 0000644 00000013537 15073235522 0011156 0 ustar 00 <?php /** * Add additional system status report sections. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Notes\Notes; defined( 'ABSPATH' ) || exit; /** * SystemStatusReport class. */ class SystemStatusReport { /** * Class instance. * * @var SystemStatus instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) ); } /** * Hooks extra necessary sections into the system status report template */ public function system_status_report() { ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="5" data-export-label="Admin"> <h2> <?php esc_html_e( 'Admin', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows details of WC Admin.', 'woocommerce' ) ); ?> </h2> </th> </tr> </thead> <tbody> <?php $this->render_features(); $this->render_daily_cron(); $this->render_options(); $this->render_notes(); $this->render_onboarding_state(); ?> </tbody> </table> <?php } /** * Render features rows. */ public function render_features() { /** * Filter the admin feature configs. * * @since 6.5.0 */ $features = apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() ); $enabled_features = array_filter( $features ); $disabled_features = array_filter( $features, function( $feature ) { return empty( $feature ); } ); ?> <tr> <td data-export-label="Enabled Features"> <?php esc_html_e( 'Enabled Features', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Which features are enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php echo esc_html( implode( ', ', array_keys( $enabled_features ) ) ) ?> </td> </tr> <tr> <td data-export-label="Disabled Features"> <?php esc_html_e( 'Disabled Features', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Which features are disabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php echo esc_html( implode( ', ', array_keys( $disabled_features ) ) ) ?> </td> </tr> <?php } /** * Render daily cron row. */ public function render_daily_cron() { $next_daily_cron = wp_next_scheduled( 'wc_admin_daily' ); ?> <tr> <td data-export-label="Daily Cron"> <?php esc_html_e( 'Daily Cron', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Is the daily cron job active, when does it next run?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( empty( $next_daily_cron ) ) { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not scheduled', 'woocommerce' ) . '</mark>'; } else { echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> Next scheduled: ' . esc_html( date_i18n( 'Y-m-d H:i:s P', $next_daily_cron ) ) . '</mark>'; } ?> </td> </tr> <?php } /** * Render option row. */ public function render_options() { $woocommerce_admin_install_timestamp = get_option( 'woocommerce_admin_install_timestamp' ); $all_options_expected = is_numeric( $woocommerce_admin_install_timestamp ) && 0 < (int) $woocommerce_admin_install_timestamp && is_array( get_option( 'woocommerce_onboarding_profile', array() ) ); ?> <tr> <td data-export-label="Options"> <?php esc_html_e( 'Options', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Do the important options return expected values?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php if ( $all_options_expected ) { echo '<mark class="yes"><span class="dashicons dashicons-yes"></mark>'; } else { echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not all expected', 'woocommerce' ) . '</mark>'; } ?> </td> </tr> <?php } /** * Render the notes row. */ public function render_notes() { $notes_count = Notes::get_notes_count(); ?> <tr> <td data-export-label="Notes"> <?php esc_html_e( 'Notes', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'How many notes in the database?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php echo esc_html( $notes_count ) ?> </td> </tr> <?php } /** * Render the onboarding state row. */ public function render_onboarding_state() { $onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() ); $onboarding_state = '-'; if ( isset( $onboarding_profile['skipped'] ) && $onboarding_profile['skipped'] ) { $onboarding_state = 'skipped'; } if ( isset( $onboarding_profile['completed'] ) && $onboarding_profile['completed'] ) { $onboarding_state = 'completed'; } ?> <tr> <td data-export-label="Onboarding"> <?php esc_html_e( 'Onboarding', 'woocommerce' ); ?>: </td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Was onboarding completed or skipped?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php echo esc_html( $onboarding_state ) ?> </td> </tr> <?php } } ActivityPanels.php 0000644 00000003120 15073235522 0010214 0 ustar 00 <?php /** * WooCommerce Activity Panel. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Notes\Notes; /** * Contains backend logic for the activity panel feature. */ class ActivityPanels { /** * Class instance. * * @var ActivityPanels instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) ); // Run after Automattic\WooCommerce\Internal\Admin\Loader. add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // New settings injection. add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 ); } /** * Adds fields so that we can store activity panel last read and open times. * * @param array $user_data_fields User data fields. * @return array */ public function add_user_data_fields( $user_data_fields ) { return array_merge( $user_data_fields, array( 'activity_panel_inbox_last_read', 'activity_panel_reviews_last_read', ) ); } /** * Add alert count to the component settings. * * @param array $settings Component settings. */ public function component_settings( $settings ) { $settings['alertCount'] = Notes::get_notes_count( array( 'error', 'update' ), array( 'unactioned' ) ); return $settings; } } CustomerEffortScoreTracks.php 0000644 00000042273 15073235522 0012404 0 ustar 00 <?php /** * WooCommerce Customer effort score tracks * * @package WooCommerce\Admin\Features */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * Triggers customer effort score on several different actions. */ class CustomerEffortScoreTracks { /** * Option name for the CES Tracks queue. */ const CES_TRACKS_QUEUE_OPTION_NAME = 'woocommerce_ces_tracks_queue'; /** * Option name for the clear CES Tracks queue for page. */ const CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME = 'woocommerce_clear_ces_tracks_queue_for_page'; /** * Option name for the set of actions that have been shown. */ const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions'; /** * Action name for product add/publish. */ const PRODUCT_ADD_PUBLISH_ACTION_NAME = 'product_add_publish'; /** * Action name for product update. */ const PRODUCT_UPDATE_ACTION_NAME = 'product_update'; /** * Action name for shop order update. */ const SHOP_ORDER_UPDATE_ACTION_NAME = 'shop_order_update'; /** * Action name for settings change. */ const SETTINGS_CHANGE_ACTION_NAME = 'settings_change'; /** * Action name for add product categories. */ const ADD_PRODUCT_CATEGORIES_ACTION_NAME = 'add_product_categories'; /** * Action name for add product tags. */ const ADD_PRODUCT_TAGS_ACTION_NAME = 'add_product_tags'; /* * Action name for add product attributes. */ const ADD_PRODUCT_ATTRIBUTES_ACTION_NAME = 'add_product_attributes'; /** * Action name for import products. */ const IMPORT_PRODUCTS_ACTION_NAME = 'import_products'; /** * Action name for search. */ const SEARCH_ACTION_NAME = 'ces_search'; /** * Label for the snackbar that appears when a user submits the survey. * * @var string */ private $onsubmit_label; /** * Constructor. Sets up filters to hook into WooCommerce. */ public function __construct() { $this->enable_survey_enqueing_if_tracking_is_enabled(); } /** * Add actions that require woocommerce_allow_tracking. */ private function enable_survey_enqueing_if_tracking_is_enabled() { // Only hook up the action handlers if in wp-admin. if ( ! is_admin() ) { return; } // Do not hook up the action handlers if a mobile device is used. if ( wp_is_mobile() ) { return; } // Only enqueue a survey if tracking is allowed. $allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ); if ( ! $allow_tracking ) { return; } add_action( 'admin_init', array( $this, 'maybe_clear_ces_tracks_queue' ) ); add_action( 'woocommerce_update_options', array( $this, 'run_on_update_options' ), 10, 3 ); add_action( 'product_cat_add_form', array( $this, 'add_script_track_product_categories' ), 10, 3 ); add_action( 'product_tag_add_form', array( $this, 'add_script_track_product_tags' ), 10, 3 ); add_action( 'woocommerce_attribute_added', array( $this, 'run_on_add_product_attributes' ), 10, 3 ); add_action( 'load-edit.php', array( $this, 'run_on_load_edit_php' ), 10, 3 ); add_action( 'product_page_product_importer', array( $this, 'run_on_product_import' ), 10, 3 ); // Only hook up the transition_post_status action handler // if on the edit page. global $pagenow; if ( 'post.php' === $pagenow ) { add_action( 'transition_post_status', array( $this, 'run_on_transition_post_status', ), 10, 3 ); } $this->onsubmit_label = __( 'Thank you for your feedback!', 'woocommerce' ); } /** * Returns a generated script for tracking tags added on edit-tags.php page. * CES survey is triggered via direct access to wc/customer-effort-score store * via wp.data.dispatch method. * * Due to lack of options to directly hook ourselves into the ajax post request * initiated by edit-tags.php page, we infer a successful request by observing * an increase of the number of rows in tags table * * @param string $action Action name for the survey. * @param string $title Title for the snackbar. * @param string $first_question The text for the first question. * @param string $second_question The text for the second question. * * @return string Generated JavaScript to append to page. */ private function get_script_track_edit_php( $action, $title, $first_question, $second_question ) { return sprintf( "(function( $ ) { 'use strict'; // Hook on submit button and sets a 500ms interval function // to determine successful add tag or otherwise. $('#addtag #submit').on( 'click', function() { const initialCount = $('.tags tbody > tr').length; const interval = setInterval( function() { if ( $('.tags tbody > tr').length > initialCount ) { // New tag detected. clearInterval( interval ); wp.data.dispatch('wc/customer-effort-score').addCesSurvey({ action: '%s', title: '%s', firstQuestion: '%s', secondQuestion: '%s', onsubmitLabel: '%s' }); } else { // Form is no longer loading, most likely failed. if ( $( '#addtag .submit .spinner.is-active' ).length < 1 ) { clearInterval( interval ); } } }, 500 ); }); })( jQuery );", esc_js( $action ), esc_js( $title ), esc_js( $first_question ), esc_js( $second_question ), esc_js( $this->onsubmit_label ) ); } /** * Get the current published product count. * * @return integer The current published product count. */ private function get_product_count() { $query = new \WC_Product_Query( array( 'limit' => 1, 'paginate' => true, 'return' => 'ids', 'status' => array( 'publish' ), ) ); $products = $query->get_products(); $product_count = intval( $products->total ); return $product_count; } /** * Get the current shop order count. * * @return integer The current shop order count. */ private function get_shop_order_count() { $query = new \WC_Order_Query( array( 'limit' => 1, 'paginate' => true, 'return' => 'ids', ) ); $shop_orders = $query->get_orders(); $shop_order_count = intval( $shop_orders->total ); return $shop_order_count; } /** * Return whether the action has already been shown. * * @param string $action The action to check. * * @return bool Whether the action has already been shown. */ private function has_been_shown( $action ) { $shown_for_features = get_option( self::SHOWN_FOR_ACTIONS_OPTION_NAME, array() ); $has_been_shown = in_array( $action, $shown_for_features, true ); return $has_been_shown; } /** * Enqueue the item to the CES tracks queue. * * @param array $item The item to enqueue. */ private function enqueue_to_ces_tracks( $item ) { $queue = get_option( self::CES_TRACKS_QUEUE_OPTION_NAME, array() ); $has_duplicate = array_filter( $queue, function ( $queue_item ) use ( $item ) { return $queue_item['action'] === $item['action']; } ); if ( $has_duplicate ) { return; } $queue[] = $item; update_option( self::CES_TRACKS_QUEUE_OPTION_NAME, $queue ); } /** * Enqueue the CES survey on using search dynamically. * * @param string $search_area Search area such as "product" or "shop_order". * @param string $page_now Value of window.pagenow. * @param string $admin_page Value of window.adminpage. */ public function enqueue_ces_survey_for_search( $search_area, $page_now, $admin_page ) { if ( $this->has_been_shown( self::SEARCH_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::SEARCH_ACTION_NAME, 'title' => __( 'How easy was it to use search?', 'woocommerce' ), 'firstQuestion' => __( 'The search feature in WooCommerce is easy to use.', 'woocommerce' ), 'secondQuestion' => __( 'The search\'s functionality meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => $page_now, 'adminpage' => $admin_page, 'props' => (object) array( 'search_area' => $search_area, ), ) ); } /** * Hook into the post status lifecycle, to detect relevant user actions * that we want to survey about. * * @param string $new_status The new status. * @param string $old_status The old status. * @param Post $post The post. */ public function run_on_transition_post_status( $new_status, $old_status, $post ) { if ( 'product' === $post->post_type ) { $this->maybe_enqueue_ces_survey_for_product( $new_status, $old_status ); } elseif ( 'shop_order' === $post->post_type ) { $this->enqueue_ces_survey_for_edited_shop_order(); } } /** * Maybe enqueue the CES survey, if product is being added or edited. * * @param string $new_status The new status. * @param string $old_status The old status. */ private function maybe_enqueue_ces_survey_for_product( $new_status, $old_status ) { if ( 'publish' !== $new_status ) { return; } if ( 'publish' !== $old_status ) { $this->enqueue_ces_survey_for_new_product(); } else { $this->enqueue_ces_survey_for_edited_product(); } } /** * Enqueue the CES survey trigger for a new product. */ private function enqueue_ces_survey_for_new_product() { if ( $this->has_been_shown( self::PRODUCT_ADD_PUBLISH_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::PRODUCT_ADD_PUBLISH_ACTION_NAME, 'title' => __( 'How easy was it to add a product?', 'woocommerce' ), 'firstQuestion' => __( 'The product creation screen is easy to use.', 'woocommerce' ), 'secondQuestion' => __( 'The product creation screen\'s functionality meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product', 'adminpage' => 'post-php', 'props' => array( 'product_count' => $this->get_product_count(), ), ) ); } /** * Enqueue the CES survey trigger for an existing product. */ private function enqueue_ces_survey_for_edited_product() { if ( $this->has_been_shown( self::PRODUCT_UPDATE_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::PRODUCT_UPDATE_ACTION_NAME, 'title' => __( 'How easy was it to edit your product?', 'woocommerce' ), 'firstQuestion' => __( 'The product update process is easy to complete.', 'woocommerce' ), 'secondQuestion' => __( 'The product update process meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product', 'adminpage' => 'post-php', 'props' => array( 'product_count' => $this->get_product_count(), ), ) ); } /** * Enqueue the CES survey trigger for an existing shop order. */ private function enqueue_ces_survey_for_edited_shop_order() { if ( $this->has_been_shown( self::SHOP_ORDER_UPDATE_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::SHOP_ORDER_UPDATE_ACTION_NAME, 'title' => __( 'How easy was it to update an order?', 'woocommerce' ), 'firstQuestion' => __( 'The order details screen is easy to use.', 'woocommerce' ), 'secondQuestion' => __( 'The order details screen\'s functionality meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'shop_order', 'adminpage' => 'post-php', 'props' => array( 'order_count' => $this->get_shop_order_count(), ), ) ); } /** * Maybe clear the CES tracks queue, executed on every page load. If the * clear option is set it clears the queue. In practice, this executes a * page load after the queued CES tracks are displayed on the client, which * sets the clear option. */ public function maybe_clear_ces_tracks_queue() { $clear_ces_tracks_queue_for_page = get_option( self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME, false ); if ( ! $clear_ces_tracks_queue_for_page ) { return; } $queue = get_option( self::CES_TRACKS_QUEUE_OPTION_NAME, array() ); $remaining_items = array_filter( $queue, function ( $item ) use ( $clear_ces_tracks_queue_for_page ) { return $clear_ces_tracks_queue_for_page['pagenow'] !== $item['pagenow'] || $clear_ces_tracks_queue_for_page['adminpage'] !== $item['adminpage']; } ); update_option( self::CES_TRACKS_QUEUE_OPTION_NAME, array_values( $remaining_items ) ); update_option( self::CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME, false ); } /** * Appends a script to footer to trigger CES on adding product categories. */ public function add_script_track_product_categories() { if ( $this->has_been_shown( self::ADD_PRODUCT_CATEGORIES_ACTION_NAME ) ) { return; } wc_enqueue_js( $this->get_script_track_edit_php( self::ADD_PRODUCT_CATEGORIES_ACTION_NAME, __( 'How easy was it to add product category?', 'woocommerce' ), __( 'The product category details screen is easy to use.', 'woocommerce' ), __( "The product category details screen's functionality meets my needs.", 'woocommerce' ) ) ); } /** * Appends a script to footer to trigger CES on adding product tags. */ public function add_script_track_product_tags() { if ( $this->has_been_shown( self::ADD_PRODUCT_TAGS_ACTION_NAME ) ) { return; } wc_enqueue_js( $this->get_script_track_edit_php( self::ADD_PRODUCT_TAGS_ACTION_NAME, __( 'How easy was it to add a product tag?', 'woocommerce' ), __( 'The product tag details screen is easy to use.', 'woocommerce' ), __( "The product tag details screen's functionality meets my needs.", 'woocommerce' ) ) ); } /** * Maybe enqueue the CES survey on product import, if step is done. */ public function run_on_product_import() { // We're only interested in when the importer completes. if ( empty( $_GET['step'] ) || 'done' !== $_GET['step'] ) { // phpcs:ignore CSRF ok. return; } if ( $this->has_been_shown( self::IMPORT_PRODUCTS_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::IMPORT_PRODUCTS_ACTION_NAME, 'title' => __( 'How easy was it to import products?', 'woocommerce' ), 'firstQuestion' => __( 'The product import process is easy to complete.', 'woocommerce' ), 'secondQuestion' => __( 'The product import process meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product_page_product_importer', 'adminpage' => 'product_page_product_importer', 'props' => (object) array(), ) ); } /** * Enqueue the CES survey trigger for setting changes. */ public function run_on_update_options() { // $current_tab is set when WC_Admin_Settings::save_settings is called. global $current_tab; global $current_section; if ( $this->has_been_shown( self::SETTINGS_CHANGE_ACTION_NAME ) ) { return; } $props = array( 'settings_area' => $current_tab, ); if ( $current_section ) { $props['settings_section'] = $current_section; } $this->enqueue_to_ces_tracks( array( 'action' => self::SETTINGS_CHANGE_ACTION_NAME, 'title' => __( 'How easy was it to update your settings?', 'woocommerce' ), 'firstQuestion' => __( 'The settings screen is easy to use.', 'woocommerce' ), 'secondQuestion' => __( 'The settings screen\'s functionality meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'woocommerce_page_wc-settings', 'adminpage' => 'woocommerce_page_wc-settings', 'props' => (object) $props, ) ); } /** * Enqueue the CES survey on adding new product attributes. */ public function run_on_add_product_attributes() { if ( $this->has_been_shown( self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME ) ) { return; } $this->enqueue_to_ces_tracks( array( 'action' => self::ADD_PRODUCT_ATTRIBUTES_ACTION_NAME, 'title' => __( 'How easy was it to add a product attribute?', 'woocommerce' ), 'firstQuestion' => __( 'Product attributes are easy to use.', 'woocommerce' ), 'secondQuestion' => __( 'Product attributes\' functionality meets my needs.', 'woocommerce' ), 'onsubmit_label' => $this->onsubmit_label, 'pagenow' => 'product_page_product_attributes', 'adminpage' => 'product_page_product_attributes', 'props' => (object) array(), ) ); } /** * Determine on initiating CES survey on searching for product or orders. */ public function run_on_load_edit_php() { $allowed_types = array( 'product', 'shop_order' ); $post_type = get_current_screen()->post_type; // We're only interested for certain post types. if ( ! in_array( $post_type, $allowed_types, true ) ) { return; } // Determine whether request is search by "s" GET parameter. if ( empty( $_GET['s'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return; } $page_now = 'edit-' . $post_type; $this->enqueue_ces_survey_for_search( $post_type, $page_now, 'edit-php' ); } } WCAdminSharedSettings.php 0000644 00000003006 15073235522 0011412 0 ustar 00 <?php /** * Manages the WC Admin settings that need to be pre-loaded. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * \Automattic\WooCommerce\Internal\Admin\WCAdminSharedSettings class. */ class WCAdminSharedSettings { /** * Settings prefix used for the window.wcSettings object. * * @var string */ private $settings_prefix = 'admin'; /** * Class instance. * * @var WCAdminSharedSettings instance */ protected static $instance = null; /** * Hook into WooCommerce Blocks. */ protected function __construct() { if ( did_action( 'woocommerce_blocks_loaded' ) ) { $this->on_woocommerce_blocks_loaded(); } else { add_action( 'woocommerce_blocks_loaded', array( $this, 'on_woocommerce_blocks_loaded' ), 10 ); } } /** * Get class instance. * * @return object Instance. */ public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Adds settings to the Blocks AssetDataRegistry when woocommerce_blocks is loaded. * * @return void */ public function on_woocommerce_blocks_loaded() { if ( class_exists( '\Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry' ) ) { \Automattic\WooCommerce\Blocks\Package::container()->get( \Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class )->add( $this->settings_prefix, function() { return apply_filters( 'woocommerce_admin_shared_settings', array() ); }, true ); } } } ProductReviews/ReviewsUtil.php 0000644 00000001535 15073235522 0012534 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; /** * A utility class for handling comments that are product reviews. */ class ReviewsUtil { /** * Removes product reviews from the edit-comments page to fix the "Mine" tab counter. * * @param array|mixed $clauses A compacted array of comment query clauses. * @return array|mixed */ public static function comments_clauses_without_product_reviews( $clauses ) { global $wpdb, $current_screen; if ( isset( $current_screen->base ) && 'edit-comments' === $current_screen->base ) { $clauses['join'] .= " LEFT JOIN {$wpdb->posts} AS wp_posts_to_exclude_reviews ON comment_post_ID = wp_posts_to_exclude_reviews.ID "; $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " wp_posts_to_exclude_reviews.post_type NOT IN ('product') "; } return $clauses; } } ProductReviews/Reviews.php 0000644 00000052471 15073235522 0011703 0 ustar 00 <?php /** * Products > Reviews */ namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use WP_Ajax_Response; use WP_Comment; use WP_Screen; /** * Handles backend logic for the Reviews component. */ class Reviews { use AccessiblePrivateMethods; /** * Admin page identifier. */ const MENU_SLUG = 'product-reviews'; /** * Reviews page hook name. * * @var string|null */ protected $reviews_page_hook = null; /** * Reviews list table instance. * * @var ReviewsListTable|null */ protected $reviews_list_table; /** * Constructor. */ public function __construct() { self::add_action( 'admin_menu', [ $this, 'add_reviews_page' ] ); self::add_action( 'admin_enqueue_scripts', [ $this, 'load_javascript' ] ); // These ajax callbacks need a low priority to ensure they run before their WordPress core counterparts. self::add_action( 'wp_ajax_edit-comment', [ $this, 'handle_edit_review' ], -1 ); self::add_action( 'wp_ajax_replyto-comment', [ $this, 'handle_reply_to_review' ], -1 ); self::add_filter( 'parent_file', [ $this, 'edit_review_parent_file' ] ); self::add_filter( 'gettext', [ $this, 'edit_comments_screen_text' ], 10, 2 ); self::add_action( 'admin_notices', [ $this, 'display_notices' ] ); } /** * Gets the required capability to access the reviews page and manage product reviews. * * @param string $context The context for which the capability is needed (e.g. `view` or `moderate`). * @return string */ public static function get_capability( string $context = 'view' ) : string { /** * Filters whether the current user can manage product reviews. * * This is aligned to {@see \wc_rest_check_product_reviews_permissions()} * * @since 6.7.0 * * @param string $capability The capability (defaults to `moderate_comments` for viewing and `edit_products` for editing). * @param string $context The context for which the capability is needed. */ return (string) apply_filters( 'woocommerce_product_reviews_page_capability', $context === 'view' ? 'moderate_comments' : 'edit_products', $context ); } /** * Registers the Product Reviews submenu page. * * @return void */ private function add_reviews_page() : void { $this->reviews_page_hook = add_submenu_page( 'edit.php?post_type=product', __( 'Reviews', 'woocommerce' ), __( 'Reviews', 'woocommerce' ) . $this->get_pending_count_bubble(), static::get_capability(), static::MENU_SLUG, [ $this, 'render_reviews_list_table' ] ); self::add_action( "load-{$this->reviews_page_hook}", array( $this, 'load_reviews_screen' ) ); } /** * Retrieves the URL to the product reviews page. * * @return string */ public static function get_reviews_page_url() : string { return add_query_arg( [ 'post_type' => 'product', 'page' => static::MENU_SLUG, ], admin_url( 'edit.php' ) ); } /** * Determines whether the current page is the reviews page. * * @global WP_Screen $current_screen * * @return bool */ public function is_reviews_page() : bool { global $current_screen; return isset( $current_screen->base ) && $current_screen->base === 'product_page_' . static::MENU_SLUG; } /** * Loads the JavaScript required for inline replies and quick edit. * * @return void */ private function load_javascript() : void { if ( $this->is_reviews_page() ) { wp_enqueue_script( 'admin-comments' ); enqueue_comment_hotkeys_js(); } } /** * Determines if the object is a review or a reply to a review. * * @param WP_Comment|mixed $object Object to check. * @return bool */ protected function is_review_or_reply( $object ) : bool { $is_review_or_reply = $object instanceof WP_Comment && in_array( $object->comment_type, [ 'review', 'comment' ], true ) && get_post_type( $object->comment_post_ID ) === 'product'; /** * Filters whether the object is a review or a reply to a review. * * @since 6.7.0 * * @param bool $is_review_or_reply Whether the object in context is a review or a reply to a review. * @param WP_Comment|mixed $object The object in context. */ return (bool) apply_filters( 'woocommerce_product_reviews_is_product_review_or_reply', $is_review_or_reply, $object ); } /** * Ajax callback for editing a review. * * This functionality is taken from {@see wp_ajax_edit_comment()} and is largely copy and pasted. The only thing * we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need * to use our own {@see ReviewsListTable} class to support our custom columns. * * This ajax callback is registered with a lower priority than WordPress core's so that our code can run * first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method * to allow the WordPress core callback to take over. * * @return void */ private function handle_edit_review(): void { // Don't interfere with comment functionality relating to the reviews meta box within the product editor. if ( sanitize_text_field( wp_unslash( $_POST['mode'] ?? '' ) ) === 'single' ) { return; } check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ); $comment_id = isset( $_POST['comment_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_ID'] ) ) : 0; if ( empty( $comment_id ) || ! current_user_can( 'edit_comment', $comment_id ) ) { wp_die( -1 ); } $review = get_comment( $comment_id ); // Bail silently if this is not a review, or a reply to a review. That allows `wp_ajax_edit_comment()` to handle any further actions. if ( ! $this->is_review_or_reply( $review ) ) { return; } if ( empty( $review->comment_ID ) ) { wp_die( -1 ); } if ( empty( $_POST['content'] ) ) { wp_die( esc_html__( 'Error: Please type your review text.', 'woocommerce' ) ); } if ( isset( $_POST['status'] ) ) { $_POST['comment_status'] = sanitize_text_field( wp_unslash( $_POST['status'] ) ); } $updated = edit_comment(); if ( is_wp_error( $updated ) ) { wp_die( esc_html( $updated->get_error_message() ) ); } $position = isset( $_POST['position'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['position'] ) ) : -1; $wp_list_table = $this->make_reviews_list_table(); ob_start(); $wp_list_table->single_row( $review ); $review_list_item = ob_get_clean(); $x = new WP_Ajax_Response(); $x->add( array( 'what' => 'edit_comment', 'id' => $review->comment_ID, 'data' => $review_list_item, 'position' => $position, ) ); $x->send(); } /** * Ajax callback for replying to a review inline. * * This functionality is taken from {@see wp_ajax_replyto_comment()} and is largely copy and pasted. The only thing * we want to change is the review row HTML in the response. WordPress core uses a comment list table and we need * to use our own {@see ReviewsListTable} class to support our custom columns. * * This ajax callback is registered with a lower priority than WordPress core's so that our code can run * first. If the supplied comment ID is not a review or a reply to a review, then we `return` early from this method * to allow the WordPress core callback to take over. * * @return void */ private function handle_reply_to_review() : void { // Don't interfere with comment functionality relating to the reviews meta box within the product editor. if ( sanitize_text_field( wp_unslash( $_POST['mode'] ?? '' ) ) === 'single' ) { return; } check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ); $comment_post_ID = isset( $_POST['comment_post_ID'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['comment_post_ID'] ) ) : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase $post = get_post( $comment_post_ID ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase if ( ! $post ) { wp_die( -1 ); } // Inline Review replies will use the `detail` mode. If that's not what we have, then let WordPress core take over. if ( isset( $_REQUEST['mode'] ) && $_REQUEST['mode'] === 'dashboard' ) { return; } // If this is not a a reply to a review, bail silently to let WordPress core take over. if ( get_post_type( $post ) !== 'product' ) { return; } if ( ! current_user_can( 'edit_post', $comment_post_ID ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase wp_die( -1 ); } if ( empty( $post->post_status ) ) { wp_die( 1 ); } elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) { wp_die( esc_html__( 'Error: You can\'t reply to a review on a draft product.', 'woocommerce' ) ); } $user = wp_get_current_user(); if ( $user->exists() ) { $user_ID = $user->ID; $comment_author = wp_slash( $user->display_name ); $comment_author_email = wp_slash( $user->user_email ); $comment_author_url = wp_slash( $user->user_url ); // WordPress core already sanitizes `content` during the `pre_comment_content` hook, which is why it's not needed here, {@see wp_filter_comment()} and {@see kses_init_filters()}. $comment_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $comment_type = isset( $_POST['comment_type'] ) ? sanitize_text_field( wp_unslash( $_POST['comment_type'] ) ) : 'comment'; if ( current_user_can( 'unfiltered_html' ) ) { if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) { $_POST['_wp_unfiltered_html_comment'] = ''; } if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) { kses_remove_filters(); // Start with a clean slate. kses_init_filters(); // Set up the filters. remove_filter( 'pre_comment_content', 'wp_filter_post_kses' ); add_filter( 'pre_comment_content', 'wp_filter_kses' ); } } } else { wp_die( esc_html__( 'Sorry, you must be logged in to reply to a review.', 'woocommerce' ) ); } if ( $comment_content === '' ) { wp_die( esc_html__( 'Error: Please type your reply text.', 'woocommerce' ) ); } $comment_parent = 0; if ( isset( $_POST['comment_ID'] ) ) { $comment_parent = absint( wp_unslash( $_POST['comment_ID'] ) ); } $comment_auto_approved = false; $commentdata = compact( 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID' ); // Automatically approve parent comment. if ( ! empty( $_POST['approve_parent'] ) ) { $parent = get_comment( $comment_parent ); if ( $parent && $parent->comment_approved === '0' && $parent->comment_post_ID === $comment_post_ID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) { wp_die( -1 ); } if ( wp_set_comment_status( $parent, 'approve' ) ) { $comment_auto_approved = true; } } } $comment_id = wp_new_comment( $commentdata ); if ( is_wp_error( $comment_id ) ) { wp_die( esc_html( $comment_id->get_error_message() ) ); } $comment = get_comment( $comment_id ); if ( ! $comment ) { wp_die( 1 ); } $position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1'; ob_start(); $wp_list_table = $this->make_reviews_list_table(); $wp_list_table->single_row( $comment ); $comment_list_item = ob_get_clean(); $response = array( 'what' => 'comment', 'id' => $comment->comment_ID, 'data' => $comment_list_item, 'position' => $position, ); $counts = wp_count_comments(); $response['supplemental'] = array( 'in_moderation' => $counts->moderated, 'i18n_comments_text' => sprintf( /* translators: %s: Number of reviews. */ _n( '%s Review', '%s Reviews', $counts->approved, 'woocommerce' ), number_format_i18n( $counts->approved ) ), 'i18n_moderation_text' => sprintf( /* translators: %s: Number of reviews. */ _n( '%s Review in moderation', '%s Reviews in moderation', $counts->moderated, 'woocommerce' ), number_format_i18n( $counts->moderated ) ), ); if ( $comment_auto_approved && isset( $parent ) ) { $response['supplemental']['parent_approved'] = $parent->comment_ID; $response['supplemental']['parent_post_id'] = $parent->comment_post_ID; } $x = new WP_Ajax_Response(); $x->add( $response ); $x->send(); } /** * Displays notices on the Reviews page. * * @return void */ protected function display_notices() : void { if ( $this->is_reviews_page() ) { $this->maybe_display_reviews_bulk_action_notice(); } } /** * May display the bulk action admin notice. * * @return void */ protected function maybe_display_reviews_bulk_action_notice() : void { $messages = $this->get_bulk_action_notice_messages(); echo ! empty( $messages ) ? '<div id="moderated" class="updated"><p>' . implode( "<br/>\n", $messages ) . '</p></div>' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Gets the applicable bulk action admin notice messages. * * @return array */ protected function get_bulk_action_notice_messages() : array { $approved = isset( $_REQUEST['approved'] ) ? (int) $_REQUEST['approved'] : 0; $unapproved = isset( $_REQUEST['unapproved'] ) ? (int) $_REQUEST['unapproved'] : 0; $deleted = isset( $_REQUEST['deleted'] ) ? (int) $_REQUEST['deleted'] : 0; $trashed = isset( $_REQUEST['trashed'] ) ? (int) $_REQUEST['trashed'] : 0; $untrashed = isset( $_REQUEST['untrashed'] ) ? (int) $_REQUEST['untrashed'] : 0; $spammed = isset( $_REQUEST['spammed'] ) ? (int) $_REQUEST['spammed'] : 0; $unspammed = isset( $_REQUEST['unspammed'] ) ? (int) $_REQUEST['unspammed'] : 0; $messages = []; if ( $approved > 0 ) { /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review approved', '%s reviews approved', $approved, 'woocommerce' ), $approved ); } if ( $unapproved > 0 ) { /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review unapproved', '%s reviews unapproved', $unapproved, 'woocommerce' ), $unapproved ); } if ( $spammed > 0 ) { $ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0; /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review marked as spam.', '%s reviews marked as spam.', $spammed, 'woocommerce' ), $spammed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=unspam&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />'; } if ( $unspammed > 0 ) { /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review restored from the spam', '%s reviews restored from the spam', $unspammed, 'woocommerce' ), $unspammed ); } if ( $trashed > 0 ) { $ids = isset( $_REQUEST['ids'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ids'] ) ) : 0; /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review moved to the Trash.', '%s reviews moved to the Trash.', $trashed, 'woocommerce' ), $trashed ) . ' <a href="' . esc_url( wp_nonce_url( "edit-comments.php?doaction=undo&action=untrash&ids=$ids", 'bulk-comments' ) ) . '">' . __( 'Undo', 'woocommerce' ) . '</a><br />'; } if ( $untrashed > 0 ) { /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review restored from the Trash', '%s reviews restored from the Trash', $untrashed, 'woocommerce' ), $untrashed ); } if ( $deleted > 0 ) { /* translators: %s is an integer higher than 0 (1, 2, 3...) */ $messages[] = sprintf( _n( '%s review permanently deleted', '%s reviews permanently deleted', $deleted, 'woocommerce' ), $deleted ); } return $messages; } /** * Counts the number of pending product reviews/replies, and returns the notification bubble if there's more than zero. * * @return string Empty string if there are no pending reviews, or bubble HTML if there are. */ protected function get_pending_count_bubble() : string { $count = (int) get_comments( [ 'type__in' => [ 'review', 'comment' ], 'status' => '0', 'post_type' => 'product', 'count' => true, ] ); /** * Provides an opportunity to alter the pending comment count used within * the product reviews admin list table. * * @since 7.0.0 * * @param array $count Current count of comments pending review. */ $count = apply_filters( 'woocommerce_product_reviews_pending_count', $count ); if ( empty( $count ) ) { return ''; } return ' <span class="awaiting-mod count-' . esc_attr( $count ) . '"><span class="pending-count">' . esc_html( $count ) . '</span></span>'; } /** * Highlights Product -> Reviews admin menu item when editing a review or a reply to a review. * * @global string $submenu_file * * @param string|mixed $parent_file Parent menu item. * @return string */ protected function edit_review_parent_file( $parent_file ) { global $submenu_file, $current_screen; if ( isset( $current_screen->id, $_GET['c'] ) && $current_screen->id === 'comment' ) { $comment_id = absint( $_GET['c'] ); $comment = get_comment( $comment_id ); if ( isset( $comment->comment_parent ) && $comment->comment_parent > 0 ) { $comment = get_comment( $comment->comment_parent ); } if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) { $parent_file = 'edit.php?post_type=product'; $submenu_file = 'product-reviews'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } } return $parent_file; } /** * Replaces Edit/Moderate Comment title/headline with Edit Review, when editing/moderating a review. * * @param string|mixed $translation Translated text. * @param string|mixed $text Text to translate. * @return string|mixed Translated text. */ protected function edit_comments_screen_text( $translation, $text ) { global $comment; // Bail out if not a text we should replace. if ( ! in_array( $text, [ 'Edit Comment', 'Moderate Comment' ], true ) ) { return $translation; } // Try to get comment from query params when not in context already. if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && $_GET['action'] === 'editcomment' ) { $comment_id = absint( $_GET['c'] ); $comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } $is_reply = isset( $comment->comment_parent ) && $comment->comment_parent > 0; // Only replace the translated text if we are editing a comment left on a product (ie. a review). if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) { if ( $text === 'Edit Comment' ) { $translation = $is_reply ? __( 'Edit Review Reply', 'woocommerce' ) : __( 'Edit Review', 'woocommerce' ); } elseif ( $text === 'Moderate Comment' ) { $translation = $is_reply ? __( 'Moderate Review Reply', 'woocommerce' ) : __( 'Moderate Review', 'woocommerce' ); } } return $translation; } /** * Returns a new instance of `ReviewsListTable`, with the screen argument specified. * * @return ReviewsListTable */ protected function make_reviews_list_table() : ReviewsListTable { return new ReviewsListTable( [ 'screen' => $this->reviews_page_hook ? $this->reviews_page_hook : 'product_page_product-reviews' ] ); } /** * Initializes the list table. * * @return void */ protected function load_reviews_screen() : void { $this->reviews_list_table = $this->make_reviews_list_table(); $this->reviews_list_table->process_bulk_action(); } /** * Renders the Reviews page. * * @return void */ public function render_reviews_list_table() : void { $this->reviews_list_table->prepare_items(); ob_start(); ?> <div class="wrap"> <h2><?php echo esc_html( get_admin_page_title() ); ?></h2> <?php $this->reviews_list_table->views(); ?> <form id="reviews-filter" method="get"> <?php $page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : static::MENU_SLUG; ?> <input type="hidden" name="page" value="<?php echo esc_attr( $page ); ?>" /> <input type="hidden" name="post_type" value="product" /> <input type="hidden" name="pagegen_timestamp" value="<?php echo esc_attr( current_time( 'mysql', true ) ); ?>" /> <?php $this->reviews_list_table->search_box( __( 'Search Reviews', 'woocommerce' ), 'reviews' ); ?> <?php $this->reviews_list_table->display(); ?> </form> </div> <?php wp_comment_reply( '-1', true, 'detail' ); wp_comment_trashnotice(); /** * Filters the contents of the product reviews list table output. * * @since 6.7.0 * * @param string $output The HTML output of the list table. * @param ReviewsListTable $reviews_list_table The reviews list table instance. */ echo apply_filters( 'woocommerce_product_reviews_list_table', ob_get_clean(), $this->reviews_list_table ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } ProductReviews/ReviewsListTable.php 0000644 00000133171 15073235522 0013504 0 ustar 00 <?php /** * Product > Reviews */ namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; use WC_Product; use WP_Comment; use WP_Comments_List_Table; use WP_List_Table; use WP_Post; /** * Handles the Product Reviews page. */ class ReviewsListTable extends WP_List_Table { /** * Memoization flag to determine if the current user can edit the current review. * * @var bool */ private $current_user_can_edit_review = false; /** * Memoization flag to determine if the current user can moderate reviews. * * @var bool */ private $current_user_can_moderate_reviews; /** * Current rating of reviews to display. * * @var int */ private $current_reviews_rating = 0; /** * Current product the reviews should be displayed for. * * @var WC_Product|null Product or null for all products. */ private $current_product_for_reviews; /** * Constructor. * * @param array|string $args Array or string of arguments. */ public function __construct( $args = [] ) { parent::__construct( wp_parse_args( $args, [ 'plural' => 'product-reviews', 'singular' => 'product-review', ] ) ); $this->current_user_can_moderate_reviews = current_user_can( Reviews::get_capability( 'moderate' ) ); } /** * Prepares reviews for display. * * @return void */ public function prepare_items() : void { $this->set_review_status(); $this->set_review_type(); $this->current_reviews_rating = isset( $_REQUEST['review_rating'] ) ? absint( $_REQUEST['review_rating'] ) : 0; $this->set_review_product(); $args = [ 'number' => $this->get_per_page(), 'post_type' => 'product', ]; // Include the order & orderby arguments. $args = wp_parse_args( $this->get_sort_arguments(), $args ); // Handle the review item types filter. $args = wp_parse_args( $this->get_filter_type_arguments(), $args ); // Handle the reviews rating filter. $args = wp_parse_args( $this->get_filter_rating_arguments(), $args ); // Handle the review product filter. $args = wp_parse_args( $this->get_filter_product_arguments(), $args ); // Include the review status arguments. $args = wp_parse_args( $this->get_status_arguments(), $args ); // Include the search argument. $args = wp_parse_args( $this->get_search_arguments(), $args ); // Include the offset argument. $args = wp_parse_args( $this->get_offset_arguments(), $args ); /** * Provides an opportunity to alter the comment query arguments used within * the product reviews admin list table. * * @since 7.0.0 * * @param array $args Comment query args. */ $args = (array) apply_filters( 'woocommerce_product_reviews_list_table_prepare_items_args', $args ); $comments = get_comments( $args ); update_comment_cache( $comments ); $this->items = $comments; $this->set_pagination_args( [ 'total_items' => get_comments( $this->get_total_comments_arguments( $args ) ), 'per_page' => $this->get_per_page(), ] ); } /** * Returns the number of items to show per page. * * @return int Customized per-page value if available, or 20 as the default. */ protected function get_per_page() : int { return $this->get_items_per_page( 'edit_comments_per_page' ); } /** * Sets the product to filter reviews by. * * @return void */ protected function set_review_product() : void { $product_id = isset( $_REQUEST['product_id'] ) ? absint( $_REQUEST['product_id'] ) : null; $product = $product_id ? wc_get_product( $product_id ) : null; if ( $product instanceof WC_Product ) { $this->current_product_for_reviews = $product; } } /** * Sets the `$comment_status` global based on the current request. * * @global string $comment_status * * @return void */ protected function set_review_status() : void { global $comment_status; $comment_status = sanitize_text_field( wp_unslash( $_REQUEST['comment_status'] ?? 'all' ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited if ( ! in_array( $comment_status, [ 'all', 'moderated', 'approved', 'spam', 'trash' ], true ) ) { $comment_status = 'all'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } } /** * Sets the `$comment_type` global based on the current request. * * @global string $comment_type * * @return void */ protected function set_review_type() : void { global $comment_type; $review_type = sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ?? 'all' ) ); if ( 'all' !== $review_type && ! empty( $review_type ) ) { $comment_type = $review_type; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } } /** * Builds the `orderby` and `order` arguments based on the current request. * * @return array */ protected function get_sort_arguments() : array { $orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ?? '' ) ); $order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ?? '' ) ); $args = []; if ( ! in_array( $orderby, $this->get_sortable_columns(), true ) ) { $orderby = 'comment_date_gmt'; } // If ordering by "rating", then we need to adjust to sort by meta value. if ( 'rating' === $orderby ) { $orderby = 'meta_value_num'; $args['meta_key'] = 'rating'; } if ( ! in_array( strtolower( $order ), [ 'asc', 'desc' ], true ) ) { $order = 'desc'; } return wp_parse_args( [ 'orderby' => $orderby, 'order' => strtolower( $order ), ], $args ); } /** * Builds the `type` argument based on the current request. * * @return array */ protected function get_filter_type_arguments() : array { $args = []; $item_type = isset( $_REQUEST['review_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ) ) : 'all'; if ( 'all' === $item_type ) { return $args; } $args['type'] = $item_type; return $args; } /** * Builds the `meta_query` arguments based on the current request. * * @return array */ protected function get_filter_rating_arguments() : array { $args = []; if ( empty( $this->current_reviews_rating ) ) { return $args; } $args['meta_query'] = [ [ 'key' => 'rating', 'value' => (int) $this->current_reviews_rating, 'compare' => '=', 'type' => 'NUMERIC', ], ]; return $args; } /** * Gets the `post_id` argument based on the current request. * * @return array */ public function get_filter_product_arguments() : array { $args = []; if ( $this->current_product_for_reviews instanceof WC_Product ) { $args['post_id'] = $this->current_product_for_reviews->get_id(); } return $args; } /** * Gets the `status` argument based on the current request. * * @return array */ protected function get_status_arguments() : array { $args = []; global $comment_status; if ( ! empty( $comment_status ) && 'all' !== $comment_status && array_key_exists( $comment_status, $this->get_status_filters() ) ) { $args['status'] = $this->convert_status_to_query_value( $comment_status ); } return $args; } /** * Gets the `search` argument based on the current request. * * @return array */ protected function get_search_arguments() : array { $args = []; if ( ! empty( $_REQUEST['s'] ) ) { $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); } return $args; } /** * Returns the `offset` argument based on the current request. * * @return array */ protected function get_offset_arguments() : array { $args = []; if ( isset( $_REQUEST['start'] ) ) { $args['offset'] = absint( wp_unslash( $_REQUEST['start'] ) ); } else { $args['offset'] = ( $this->get_pagenum() - 1 ) * $this->get_per_page(); } return $args; } /** * Returns the arguments used to count the total number of comments. * * @param array $default_query_args Query args for the main request. * @return array */ protected function get_total_comments_arguments( array $default_query_args ) : array { return wp_parse_args( [ 'count' => true, 'offset' => 0, 'number' => 0, ], $default_query_args ); } /** * Displays the product reviews HTML table. * * Reimplements {@see WP_Comment_::display()} but we change the ID to match the one output by {@see WP_Comments_List_Table::display()}. * This will automatically handle additional CSS for consistency with the comments page. * * @return void */ public function display() : void { $this->display_tablenav( 'top' ); $this->screen->render_screen_reader_content( 'heading_list' ); ?> <table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>"> <thead> <tr> <?php $this->print_column_headers(); ?> </tr> </thead> <tbody id="the-comment-list" data-wp-lists="list:comment"> <?php $this->display_rows_or_placeholder(); ?> </tbody> <tfoot> <tr> <?php $this->print_column_headers( false ); ?> </tr> </tfoot> </table> <?php $this->display_tablenav( 'bottom' ); } /** * Render a single row HTML. * * @global WP_Post $post * @global WP_Comment $comment * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ public function single_row( $item ) : void { global $post, $comment; // Overrides the comment global for properly rendering rows. $comment = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $the_comment_class = (string) wp_get_comment_status( $comment->comment_ID ); $the_comment_class = implode( ' ', get_comment_class( $the_comment_class, $comment->comment_ID, $comment->comment_post_ID ) ); // Sets the post for the product in context. $post = get_post( $comment->comment_post_ID ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $this->current_user_can_edit_review = current_user_can( 'edit_comment', $comment->comment_ID ); ?> <tr id="comment-<?php echo esc_attr( $comment->comment_ID ); ?>" class="comment <?php echo esc_attr( $the_comment_class ); ?>"> <?php $this->single_row_columns( $comment ); ?> </tr> <?php } /** * Generate and display row actions links. * * @see WP_Comments_List_Table::handle_row_actions() for consistency. * * @global string $comment_status Status for the current listed comments. * * @param WP_Comment|mixed $item The product review or reply in context. * @param string|mixed $column_name Current column name. * @param string|mixed $primary Primary column name. * @return string */ protected function handle_row_actions( $item, $column_name, $primary ) : string { global $comment_status; if ( $primary !== $column_name || ! $this->current_user_can_edit_review ) { return ''; } $review_status = wp_get_comment_status( $item ); $url = add_query_arg( [ 'c' => urlencode( $item->comment_ID ), ], admin_url( 'comment.php' ) ); $approve_url = wp_nonce_url( add_query_arg( 'action', 'approvecomment', $url ), "approve-comment_$item->comment_ID" ); $unapprove_url = wp_nonce_url( add_query_arg( 'action', 'unapprovecomment', $url ), "approve-comment_$item->comment_ID" ); $spam_url = wp_nonce_url( add_query_arg( 'action', 'spamcomment', $url ), "delete-comment_$item->comment_ID" ); $unspam_url = wp_nonce_url( add_query_arg( 'action', 'unspamcomment', $url ), "delete-comment_$item->comment_ID" ); $trash_url = wp_nonce_url( add_query_arg( 'action', 'trashcomment', $url ), "delete-comment_$item->comment_ID" ); $untrash_url = wp_nonce_url( add_query_arg( 'action', 'untrashcomment', $url ), "delete-comment_$item->comment_ID" ); $delete_url = wp_nonce_url( add_query_arg( 'action', 'deletecomment', $url ), "delete-comment_$item->comment_ID" ); $actions = [ 'approve' => '', 'unapprove' => '', 'reply' => '', 'quickedit' => '', 'edit' => '', 'spam' => '', 'unspam' => '', 'trash' => '', 'untrash' => '', 'delete' => '', ]; if ( $comment_status && 'all' !== $comment_status ) { if ( 'approved' === $review_status ) { $actions['unapprove'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-u vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $unapprove_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&new=unapproved" ), esc_attr__( 'Unapprove this review', 'woocommerce' ), esc_html__( 'Unapprove', 'woocommerce' ) ); } elseif ( 'unapproved' === $review_status ) { $actions['approve'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-a vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $approve_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&new=approved" ), esc_attr__( 'Approve this review', 'woocommerce' ), esc_html__( 'Approve', 'woocommerce' ) ); } } else { $actions['approve'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-a aria-button-if-js" aria-label="%s">%s</a>', esc_url( $approve_url ), esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved" ), esc_attr__( 'Approve this review', 'woocommerce' ), esc_html__( 'Approve', 'woocommerce' ) ); $actions['unapprove'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-u aria-button-if-js" aria-label="%s">%s</a>', esc_url( $unapprove_url ), esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved" ), esc_attr__( 'Unapprove this review', 'woocommerce' ), esc_html__( 'Unapprove', 'woocommerce' ) ); } if ( 'spam' !== $review_status ) { $actions['spam'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-s vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $spam_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::spam=1" ), esc_attr__( 'Mark this review as spam', 'woocommerce' ), /* translators: "Mark as spam" link. */ esc_html_x( 'Spam', 'verb', 'woocommerce' ) ); } else { $actions['unspam'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $unspam_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:unspam=1" ), esc_attr__( 'Restore this review from the spam', 'woocommerce' ), esc_html_x( 'Not Spam', 'review', 'woocommerce' ) ); } if ( 'trash' === $review_status ) { $actions['untrash'] = sprintf( '<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $untrash_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:untrash=1" ), esc_attr__( 'Restore this review from the Trash', 'woocommerce' ), esc_html__( 'Restore', 'woocommerce' ) ); } if ( 'spam' === $review_status || 'trash' === $review_status || ! EMPTY_TRASH_DAYS ) { $actions['delete'] = sprintf( '<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $delete_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::delete=1" ), esc_attr__( 'Delete this review permanently', 'woocommerce' ), esc_html__( 'Delete Permanently', 'woocommerce' ) ); } else { $actions['trash'] = sprintf( '<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>', esc_url( $trash_url ), esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::trash=1" ), esc_attr__( 'Move this review to the Trash', 'woocommerce' ), esc_html_x( 'Trash', 'verb', 'woocommerce' ) ); } if ( 'spam' !== $review_status && 'trash' !== $review_status ) { $actions['edit'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', esc_url( add_query_arg( [ 'action' => 'editcomment', 'c' => urlencode( $item->comment_ID ), ], admin_url( 'comment.php' ) ) ), esc_attr__( 'Edit this review', 'woocommerce' ), esc_html__( 'Edit', 'woocommerce' ) ); $format = '<button type="button" data-comment-id="%d" data-post-id="%d" data-action="%s" class="%s button-link" aria-expanded="false" aria-label="%s">%s</button>'; $actions['quickedit'] = sprintf( $format, esc_attr( $item->comment_ID ), esc_attr( $item->comment_post_ID ), 'edit', 'vim-q comment-inline', esc_attr__( 'Quick edit this review inline', 'woocommerce' ), esc_html__( 'Quick Edit', 'woocommerce' ) ); $actions['reply'] = sprintf( $format, esc_attr( $item->comment_ID ), esc_attr( $item->comment_post_ID ), 'replyto', 'vim-r comment-inline', esc_attr__( 'Reply to this review', 'woocommerce' ), esc_html__( 'Reply', 'woocommerce' ) ); } $always_visible = 'excerpt' === get_user_setting( 'posts_list_mode', 'list' ); $output = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">'; $i = 0; foreach ( array_filter( $actions ) as $action => $link ) { ++$i; if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i ) || 1 === $i ) { $sep = ''; } else { $sep = ' | '; } if ( ( 'reply' === $action || 'quickedit' === $action ) && ! wp_doing_ajax() ) { $action .= ' hide-if-no-js'; } elseif ( ( 'untrash' === $action && 'trash' === $review_status ) || ( 'unspam' === $action && 'spam' === $review_status ) ) { if ( '1' === get_comment_meta( $item->comment_ID, '_wp_trash_meta_status', true ) ) { $action .= ' approve'; } else { $action .= ' unapprove'; } } $output .= "<span class='$action'>$sep$link</span>"; } $output .= '</div>'; $output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . esc_html__( 'Show more details', 'woocommerce' ) . '</span></button>'; return $output; } /** * Gets the columns for the table. * * @return array Table columns and their headings. */ public function get_columns() : array { $columns = [ 'cb' => '<input type="checkbox" />', 'type' => _x( 'Type', 'review type', 'woocommerce' ), 'author' => __( 'Author', 'woocommerce' ), 'rating' => __( 'Rating', 'woocommerce' ), 'comment' => _x( 'Review', 'column name', 'woocommerce' ), 'response' => __( 'Product', 'woocommerce' ), 'date' => _x( 'Submitted on', 'column name', 'woocommerce' ), ]; /** * Filters the table columns. * * @since 6.7.0 * * @param array $columns */ return (array) apply_filters( 'woocommerce_product_reviews_table_columns', $columns ); } /** * Gets the name of the default primary column. * * @return string Name of the primary colum. */ protected function get_primary_column_name() : string { return 'comment'; } /** * Gets a list of sortable columns. * * Key is the column ID and value is which database column we perform the sorting on. * The `rating` column uses a unique key instead, as that requires sorting by meta value. * * @return array */ protected function get_sortable_columns() : array { return [ 'author' => 'comment_author', 'response' => 'comment_post_ID', 'date' => 'comment_date_gmt', 'type' => 'comment_type', 'rating' => 'rating', ]; } /** * Returns a list of available bulk actions. * * @global string $comment_status * * @return array */ protected function get_bulk_actions() : array { global $comment_status; $actions = []; if ( in_array( $comment_status, [ 'all', 'approved' ], true ) ) { $actions['unapprove'] = __( 'Unapprove', 'woocommerce' ); } if ( in_array( $comment_status, [ 'all', 'moderated' ], true ) ) { $actions['approve'] = __( 'Approve', 'woocommerce' ); } if ( in_array( $comment_status, [ 'all', 'moderated', 'approved', 'trash' ], true ) ) { $actions['spam'] = _x( 'Mark as spam', 'review', 'woocommerce' ); } if ( 'trash' === $comment_status ) { $actions['untrash'] = __( 'Restore', 'woocommerce' ); } elseif ( 'spam' === $comment_status ) { $actions['unspam'] = _x( 'Not spam', 'review', 'woocommerce' ); } if ( in_array( $comment_status, [ 'trash', 'spam' ], true ) || ! EMPTY_TRASH_DAYS ) { $actions['delete'] = __( 'Delete permanently', 'woocommerce' ); } else { $actions['trash'] = __( 'Move to Trash', 'woocommerce' ); } return $actions; } /** * Returns the current action select in bulk actions menu. * * This is overridden in order to support `delete_all` for use in {@see ReviewsListTable::process_bulk_action()} * * {@see WP_Comments_List_Table::current_action()} for reference. * * @return string|false */ public function current_action() { if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) { return 'delete_all'; } return parent::current_action(); } /** * Processes the bulk actions. * * @return void */ public function process_bulk_action() : void { if ( ! $this->current_user_can_moderate_reviews ) { return; } if ( $this->current_action() ) { check_admin_referer( 'bulk-product-reviews' ); $query_string = remove_query_arg( [ 'page', '_wpnonce' ], wp_unslash( ( $_SERVER['QUERY_STRING'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // Replace current nonce with bulk-comments nonce. $comments_nonce = wp_create_nonce( 'bulk-comments' ); $query_string = add_query_arg( '_wpnonce', $comments_nonce, $query_string ); // Redirect to edit-comments.php, which will handle processing the action for us. wp_safe_redirect( esc_url_raw( admin_url( 'edit-comments.php?' . $query_string ) ) ); exit; } elseif ( ! empty( $_GET['_wp_http_referer'] ) ) { wp_safe_redirect( remove_query_arg( [ '_wp_http_referer', '_wpnonce' ] ) ); exit; } } /** * Returns an array of supported statuses and their labels. * * @return array */ protected function get_status_filters() : array { return [ /* translators: %s: Number of reviews. */ 'all' => _nx_noop( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'moderated' => _nx_noop( 'Pending <span class="count">(%s)</span>', 'Pending <span class="count">(%s)</span>', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'approved' => _nx_noop( 'Approved <span class="count">(%s)</span>', 'Approved <span class="count">(%s)</span>', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'spam' => _nx_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'trash' => _nx_noop( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', 'product reviews', 'woocommerce' ), ]; } /** * Returns the available status filters. * * @see WP_Comments_List_Table::get_views() for consistency. * * @global int $post_id * @global string $comment_status * @global string $comment_type * * @return array An associative array of fully-formed comment status links. Includes 'All', 'Pending', 'Approved', 'Spam', and 'Trash'. */ protected function get_views() : array { global $post_id, $comment_status, $comment_type; $status_links = []; $status_labels = $this->get_status_filters(); if ( ! EMPTY_TRASH_DAYS ) { unset( $status_labels['trash'] ); } $link = $this->get_view_url( (string) $comment_type, (int) $post_id ); foreach ( $status_labels as $status => $label ) { $current_link_attributes = ''; if ( $status === $comment_status ) { $current_link_attributes = ' class="current" aria-current="page"'; } $link = add_query_arg( 'comment_status', urlencode( $status ), $link ); $number_reviews_for_status = $this->get_review_count( $status, (int) $post_id ); $count_html = sprintf( '<span class="%s-count">%s</span>', ( 'moderated' === $status ) ? 'pending' : $status, number_format_i18n( $number_reviews_for_status ) ); $status_links[ $status ] = '<a href="' . esc_url( $link ) . '"' . $current_link_attributes . '>' . sprintf( translate_nooped_plural( $label, $number_reviews_for_status ), $count_html ) . '</a>'; } return $status_links; } /** * Gets the base URL for a view, excluding the status (that should be appended). * * @param string $comment_type Comment type filter. * @param int $post_id Current post ID. * @return string */ protected function get_view_url( string $comment_type, int $post_id ) : string { $link = Reviews::get_reviews_page_url(); if ( ! empty( $comment_type ) && 'all' !== $comment_type ) { $link = add_query_arg( 'comment_type', urlencode( $comment_type ), $link ); } if ( ! empty( $post_id ) ) { $link = add_query_arg( 'p', absint( $post_id ), $link ); } return $link; } /** * Gets the number of reviews (including review replies) for a given status. * * @param string $status Status key from {@see ReviewsListTable::get_status_filters()}. * @param int $product_id ID of the product if we're filtering by product in this request. Otherwise, `0` for no product filters. * @return int */ protected function get_review_count( string $status, int $product_id ) : int { return (int) get_comments( [ 'type__in' => [ 'review', 'comment' ], 'status' => $this->convert_status_to_query_value( $status ), 'post_type' => 'product', 'post_id' => $product_id, 'count' => true, ] ); } /** * Converts a status key into its equivalent `comment_approved` database column value. * * @param string $status Status key from {@see ReviewsListTable::get_status_filters()}. * @return string */ protected function convert_status_to_query_value( string $status ) : string { // These keys exactly match the database column. if ( in_array( $status, [ 'spam', 'trash' ], true ) ) { return $status; } switch ( $status ) { case 'moderated': return '0'; case 'approved': return '1'; default: return 'all'; } } /** * Outputs the text to display when there are no reviews to display. * * @see WP_List_Table::no_items() * * @global string $comment_status * * @return void */ public function no_items() : void { global $comment_status; if ( 'moderated' === $comment_status ) { esc_html_e( 'No reviews awaiting moderation.', 'woocommerce' ); } else { esc_html_e( 'No reviews found.', 'woocommerce' ); } } /** * Renders the checkbox column. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_cb( $item ) : void { ob_start(); if ( $this->current_user_can_edit_review ) { ?> <label class="screen-reader-text" for="cb-select-<?php echo esc_attr( $item->comment_ID ); ?>"><?php esc_html_e( 'Select review', 'woocommerce' ); ?></label> <input id="cb-select-<?php echo esc_attr( $item->comment_ID ); ?>" type="checkbox" name="delete_comments[]" value="<?php echo esc_attr( $item->comment_ID ); ?>" /> <?php } echo $this->filter_column_output( 'cb', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders the review column. * * @see WP_Comments_List_Table::column_comment() for consistency. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_comment( $item ) : void { $in_reply_to = $this->get_in_reply_to_review_text( $item ); ob_start(); if ( $in_reply_to ) { echo $in_reply_to . '<br><br>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } printf( '%1$s%2$s%3$s', '<div class="comment-text">', get_comment_text( $item->comment_ID ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped '</div>' ); if ( $this->current_user_can_edit_review ) { ?> <div id="inline-<?php echo esc_attr( $item->comment_ID ); ?>" class="hidden"> <textarea class="comment" rows="1" cols="1"><?php echo esc_textarea( $item->comment_content ); ?></textarea> <div class="author-email"><?php echo esc_attr( $item->comment_author_email ); ?></div> <div class="author"><?php echo esc_attr( $item->comment_author ); ?></div> <div class="author-url"><?php echo esc_attr( $item->comment_author_url ); ?></div> <div class="comment_status"><?php echo esc_html( $item->comment_approved ); ?></div> </div> <?php } echo $this->filter_column_output( 'comment', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Gets the in-reply-to-review text. * * @param WP_Comment|mixed $reply Reply to review. * @return string */ private function get_in_reply_to_review_text( $reply ) : string { $review = $reply->comment_parent ? get_comment( $reply->comment_parent ) : null; if ( ! $review ) { return ''; } $parent_review_link = get_comment_link( $review ); $review_author_name = get_comment_author( $review ); return sprintf( /* translators: %s: Parent review link with review author name. */ ent2ncr( __( 'In reply to %s.', 'woocommerce' ) ), '<a href="' . esc_url( $parent_review_link ) . '">' . esc_html( $review_author_name ) . '</a>' ); } /** * Renders the author column. * * @see WP_Comments_List_Table::column_author() for consistency. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_author( $item ) : void { global $comment_status; $author_url = $this->get_item_author_url(); $author_url_display = $this->get_item_author_url_for_display( $author_url ); if ( get_option( 'show_avatars' ) ) { $author_avatar = get_avatar( $item, 32, 'mystery' ); } else { $author_avatar = ''; } ob_start(); echo '<strong>' . $author_avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped comment_author(); echo '</strong><br>'; if ( ! empty( $author_url ) ) : ?> <a title="<?php echo esc_attr( $author_url ); ?>" href="<?php echo esc_url( $author_url ); ?>" rel="noopener noreferrer"><?php echo esc_html( $author_url_display ); ?></a> <br> <?php endif; if ( $this->current_user_can_edit_review ) : if ( ! empty( $item->comment_author_email ) && is_email( $item->comment_author_email ) ) : ?> <a href="mailto:<?php echo esc_attr( $item->comment_author_email ); ?>"><?php echo esc_html( $item->comment_author_email ); ?></a><br> <?php endif; $link = add_query_arg( [ 's' => urlencode( get_comment_author_IP( $item->comment_ID ) ), 'page' => Reviews::MENU_SLUG, 'mode' => 'detail', ], 'admin.php' ); if ( 'spam' === $comment_status ) : $link = add_query_arg( [ 'comment_status' => 'spam' ], $link ); endif; ?> <a href="<?php echo esc_url( $link ); ?>"><?php comment_author_IP( $item->comment_ID ); ?></a> <?php endif; echo $this->filter_column_output( 'author', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Gets the item author URL. * * @return string */ private function get_item_author_url() : string { $author_url = get_comment_author_url(); $protocols = [ 'https://', 'http://' ]; if ( in_array( $author_url, $protocols ) ) { $author_url = ''; } return $author_url; } /** * Gets the item author URL for display. * * @param string $author_url The review or reply author URL (raw). * @return string */ private function get_item_author_url_for_display( $author_url ) : string { $author_url_display = untrailingslashit( preg_replace( '|^http(s)?://(www\.)?|i', '', $author_url ) ); if ( strlen( $author_url_display ) > 50 ) { $author_url_display = wp_html_excerpt( $author_url_display, 49, '…' ); } return $author_url_display; } /** * Renders the "submitted on" column. * * Note that the output is consistent with {@see WP_Comments_List_Table::column_date()}. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_date( $item ) : void { $submitted = sprintf( /* translators: 1 - Product review date, 2: Product review time. */ __( '%1$s at %2$s', 'woocommerce' ), /* translators: Review date format. See https://www.php.net/manual/datetime.format.php */ get_comment_date( __( 'Y/m/d', 'woocommerce' ), $item ), /* translators: Review time format. See https://www.php.net/manual/datetime.format.php */ get_comment_date( __( 'g:i a', 'woocommerce' ), $item ) ); ob_start(); ?> <div class="submitted-on"> <?php if ( 'approved' === wp_get_comment_status( $item ) && ! empty( $item->comment_post_ID ) ) : printf( '<a href="%1$s">%2$s</a>', esc_url( get_comment_link( $item ) ), esc_html( $submitted ) ); else : echo esc_html( $submitted ); endif; ?> </div> <?php echo $this->filter_column_output( 'date', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders the product column. * * @see WP_Comments_List_Table::column_response() for consistency. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_response( $item ) : void { $product_post = get_post(); ob_start(); if ( $product_post ) : ?> <div class="response-links"> <?php if ( current_user_can( 'edit_product', $product_post->ID ) ) : $post_link = "<a href='" . esc_url( get_edit_post_link( $product_post->ID ) ) . "' class='comments-edit-item-link'>"; $post_link .= esc_html( get_the_title( $product_post->ID ) ) . '</a>'; else : $post_link = esc_html( get_the_title( $product_post->ID ) ); endif; echo $post_link; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $post_type_object = get_post_type_object( $product_post->post_type ); ?> <a href="<?php echo esc_url( get_permalink( $product_post->ID ) ); ?>" class="comments-view-item-link"> <?php echo esc_html( $post_type_object->labels->view_item ); ?> </a> <span class="post-com-count-wrapper post-com-count-<?php echo esc_attr( $product_post->ID ); ?>"> <?php $this->comments_bubble( $product_post->ID, get_pending_comments_num( $product_post->ID ) ); ?> </span> </div> <?php endif; echo $this->filter_column_output( 'response', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders the type column. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_type( $item ) : void { $type = 'review' === $item->comment_type ? '☆ ' . __( 'Review', 'woocommerce' ) : __( 'Reply', 'woocommerce' ); echo $this->filter_column_output( 'type', esc_html( $type ), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders the rating column. * * @param WP_Comment|mixed $item Review or reply being rendered. * @return void */ protected function column_rating( $item ) : void { $rating = get_comment_meta( $item->comment_ID, 'rating', true ); ob_start(); if ( ! empty( $rating ) && is_numeric( $rating ) ) { $rating = (int) $rating; $accessibility_label = sprintf( /* translators: 1: number representing a rating */ __( '%1$d out of 5', 'woocommerce' ), $rating ); $stars = str_repeat( '★', $rating ); $stars .= str_repeat( '☆', 5 - $rating ); ?> <span aria-label="<?php echo esc_attr( $accessibility_label ); ?>"><?php echo esc_html( $stars ); ?></span> <?php } echo $this->filter_column_output( 'rating', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders any custom columns. * * @param WP_Comment|mixed $item Review or reply being rendered. * @param string|mixed $column_name Name of the column being rendered. * @return void */ protected function column_default( $item, $column_name ) : void { ob_start(); /** * Fires when the default column output is displayed for a single row. * * This action can be used to render custom columns that have been added. * * @since 6.7.0 * * @param WP_Comment $item The review or reply being rendered. */ do_action( 'woocommerce_product_reviews_table_column_' . $column_name, $item ); echo $this->filter_column_output( $column_name, ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Runs a filter hook for a given column content. * * @param string|mixed $column_name The column being output. * @param string|mixed $output The output content (may include HTML). * @param WP_Comment|mixed $item The review or reply being rendered. * @return string */ protected function filter_column_output( $column_name, $output, $item ) : string { /** * Filters the output of a column. * * @since 6.7.0 * * @param string $output The column output. * @param WP_Comment $item The product review being rendered. */ return (string) apply_filters( 'woocommerce_product_reviews_table_column_' . $column_name . '_content', $output, $item ); } /** * Renders the extra controls to be displayed between bulk actions and pagination. * * @global string $comment_status * @global string $comment_type * * @param string|mixed $which Position (top or bottom). * @return void */ protected function extra_tablenav( $which ) : void { global $comment_status, $comment_type; echo '<div class="alignleft actions">'; if ( 'top' === $which ) { ob_start(); echo '<input type="hidden" name="comment_status" value="' . esc_attr( $comment_status ?? 'all' ) . '" />'; $this->review_type_dropdown( $comment_type ); $this->review_rating_dropdown( $this->current_reviews_rating ); $this->product_search( $this->current_product_for_reviews ); echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, [ 'id' => 'post-query-submit' ] ); } if ( ( 'spam' === $comment_status || 'trash' === $comment_status ) && $this->has_items() && $this->current_user_can_moderate_reviews ) { wp_nonce_field( 'bulk-destroy', '_destroy_nonce' ); $title = 'spam' === $comment_status ? esc_attr__( 'Empty Spam', 'woocommerce' ) : esc_attr__( 'Empty Trash', 'woocommerce' ); submit_button( $title, 'apply', 'delete_all', false ); } echo '</div>'; } /** * 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' ), ) ); ?> <label class="screen-reader-text" for="filter-by-review-type"><?php esc_html_e( 'Filter by review type', 'woocommerce' ); ?></label> <select id="filter-by-review-type" name="review_type"> <?php foreach ( $item_types as $type => $label ) : ?> <option value="<?php echo esc_attr( $type ); ?>" <?php selected( $type, $current_type ); ?>><?php echo esc_html( $label ); ?></option> <?php endforeach; ?> </select> <?php } /** * Displays a review rating drop-down for filtering reviews in the Product Reviews list table. * * @param int|string|mixed $current_rating Rating to display reviews for. * @return void */ public function review_rating_dropdown( $current_rating ) : void { $rating_options = [ '0' => __( 'All ratings', 'woocommerce' ), '1' => '★', '2' => '★★', '3' => '★★★', '4' => '★★★★', '5' => '★★★★★', ]; ?> <label class="screen-reader-text" for="filter-by-review-rating"><?php esc_html_e( 'Filter by review rating', 'woocommerce' ); ?></label> <select id="filter-by-review-rating" name="review_rating"> <?php foreach ( $rating_options as $rating => $label ) : ?> <?php $title = 0 === (int) $rating ? $label : sprintf( /* translators: %s: Star rating (1-5). */ __( '%s-star rating', 'woocommerce' ), $rating ); ?> <option value="<?php echo esc_attr( $rating ); ?>" <?php selected( $rating, (string) $current_rating ); ?> title="<?php echo esc_attr( $title ); ?>"><?php echo esc_html( $label ); ?></option> <?php endforeach; ?> </select> <?php } /** * Displays a product search input for filtering reviews by product in the Product Reviews list table. * * @param WC_Product|null $current_product The current product (or null when displaying all reviews). * @return void */ protected function product_search( ?WC_Product $current_product ) : void { ?> <label class="screen-reader-text" for="filter-by-product"><?php esc_html_e( 'Filter by product', 'woocommerce' ); ?></label> <select id="filter-by-product" class="wc-product-search" name="product_id" style="width: 200px;" data-placeholder="<?php esc_attr_e( 'Search for a product…', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products" data-allow_clear="true"> <?php if ( $current_product instanceof WC_Product ) : ?> <option value="<?php echo esc_attr( $current_product->get_id() ); ?>" selected="selected"><?php echo esc_html( $current_product->get_formatted_name() ); ?></option> <?php endif; ?> </select> <?php } /** * Displays a review count bubble. * * Based on {@see WP_List_Table::comments_bubble()}, but overridden, so we can customize the URL and text output. * * @param int|mixed $post_id The product ID. * @param int|mixed $pending_comments Number of pending reviews. * * @return void */ protected function comments_bubble( $post_id, $pending_comments ) : void { $approved_review_count = get_comments_number(); $approved_reviews_number = number_format_i18n( $approved_review_count ); $pending_reviews_number = number_format_i18n( $pending_comments ); $approved_only_phrase = sprintf( /* translators: %s: Number of reviews. */ _n( '%s review', '%s reviews', $approved_review_count, 'woocommerce' ), $approved_reviews_number ); $approved_phrase = sprintf( /* translators: %s: Number of reviews. */ _n( '%s approved review', '%s approved reviews', $approved_review_count, 'woocommerce' ), $approved_reviews_number ); $pending_phrase = sprintf( /* translators: %s: Number of reviews. */ _n( '%s pending review', '%s pending reviews', $pending_comments, 'woocommerce' ), $pending_reviews_number ); if ( ! $approved_review_count && ! $pending_comments ) { // No reviews at all. printf( '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>', 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( '<span class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>', 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( '<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>', 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( '<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>', esc_html( $approved_reviews_number ), $pending_comments ? esc_html__( 'No approved reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' ) ); } if ( $pending_comments ) { printf( '<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>', 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( '<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>', 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 <?php namespace Automattic\WooCommerce\Internal\Admin\ProductReviews; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use WP_Comment_Query; use WP_Screen; /** * Tweaks the WordPress comments page to exclude reviews. */ class ReviewsCommentsOverrides { use AccessiblePrivateMethods; const REVIEWS_MOVED_NOTICE_ID = 'product_reviews_moved'; /** * Constructor. */ public function __construct() { self::add_action( 'admin_notices', array( $this, 'display_notices' ) ); self::add_filter( 'woocommerce_dismiss_admin_notice_capability', array( $this, 'get_dismiss_capability' ), 10, 2 ); self::add_filter( 'comments_list_table_query_args', array( $this, 'exclude_reviews_from_comments' ) ); } /** * Renders admin notices. */ protected function display_notices() : void { $screen = get_current_screen(); if ( empty( $screen ) || $screen->base !== 'edit-comments' ) { return; } $this->maybe_display_reviews_moved_notice(); } /** * May render an admin notice informing the user that reviews were moved to a new page. * * @return void */ protected function maybe_display_reviews_moved_notice() : void { if ( $this->should_display_reviews_moved_notice() ) { $this->display_reviews_moved_notice(); } } /** * Checks if the admin notice informing the user that reviews were moved to a new page should be displayed. * * @return bool */ protected function should_display_reviews_moved_notice() : bool { // Do not display if the user does not have the capability to see the new page. if ( ! WC()->call_function( 'current_user_can', Reviews::get_capability() ) ) { return false; } // Do not display if the current user has dismissed this notice. if ( WC()->call_function( 'get_user_meta', get_current_user_id(), 'dismissed_' . static::REVIEWS_MOVED_NOTICE_ID . '_notice', true ) ) { return false; } return true; } /** * Renders an admin notice informing the user that reviews were moved to a new page. * * @return void */ protected function display_reviews_moved_notice() : void { ?> <div class="notice notice-info is-dismissible"> <p><strong><?php esc_html_e( 'Product reviews have moved!', 'woocommerce' ); ?></strong></p> <p><?php esc_html_e( 'Product reviews can now be managed from Products > Reviews.', 'woocommerce' ); ?></p> <p class="submit"> <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&page=product-reviews' ) ); ?>" class="button-primary"><?php esc_html_e( 'Visit new location', 'woocommerce' ); ?></a> </p> <form action="<?php echo esc_url( admin_url( 'edit-comments.php' ) ); ?>" method="get"> <input type="hidden" name="wc-hide-notice" value="<?php echo esc_attr( static::REVIEWS_MOVED_NOTICE_ID ); ?>" /> <?php if ( ! empty( $_GET['comment_status'] ) ): ?> <input type="hidden" name="comment_status" value="<?php echo esc_attr( $_GET['comment_status'] ); ?>" /> <?php endif; ?> <?php if ( ! empty( $_GET['paged'] ) ): ?> <input type="hidden" name="paged" value="<?php echo esc_attr( $_GET['paged'] ); ?>" /> <?php endif; ?> <?php wp_nonce_field( 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ); ?> <button type="submit" class="notice-dismiss"> <span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'woocommerce' ); ?></span> </button> </form> </div> <?php } /** * Gets the capability required to dismiss the notice. * * This is required so that users who do not have the manage_woocommerce capability (e.g. Editors) can still dismiss * the notice displayed in the Comments page. * * @param string|mixed $default_capability The default required capability. * @param string|mixed $notice_name The notice name. * @return string */ protected function get_dismiss_capability( $default_capability, $notice_name ) { return $notice_name === self::REVIEWS_MOVED_NOTICE_ID ? Reviews::get_capability() : $default_capability; } /** * Excludes product reviews from showing in the comments page. * * @param array|mixed $args {@see WP_Comment_Query} query args. * @return array */ protected function exclude_reviews_from_comments( $args ) : array { $screen = get_current_screen(); // We only wish to intervene if the edit comments screen has been requested. if ( ! $screen instanceof WP_Screen || 'edit-comments' !== $screen->id ) { return $args; } if ( ! empty( $args['post_type'] ) && $args['post_type'] !== 'any' ) { $post_types = (array) $args['post_type']; } else { $post_types = get_post_types(); } $index = array_search( 'product', $post_types ); if ( $index !== false ) { unset( $post_types[ $index ] ); } if ( ! is_array( $args ) ) { $args = []; } $args['post_type'] = $post_types; return $args; } } Marketplace.php 0000644 00000011550 15073235522 0007513 0 ustar 00 <?php /** * WooCommerce Marketplace. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Utilities\FeaturesUtil; use Automattic\WooCommerce\Internal\Features\FeaturesController; /** * Contains backend logic for the Marketplace feature. */ class Marketplace { const MARKETPLACE_TAB_SLUG = 'woo'; /** * Class initialization, to be executed when the class is resolved by the container. */ final public function init() { if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) { /** Feature controller instance @var FeaturesController $feature_controller */ $feature_controller = wc_get_container()->get( FeaturesController::class ); $feature_controller->change_feature_enable( 'marketplace', true ); } add_action( 'admin_menu', array( $this, 'register_pages' ), 70 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); // Add a Woo Marketplace link to the plugin install action links. add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) ); add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) ); add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) ); } /** * Registers report pages. */ public function register_pages() { if ( ! function_exists( 'wc_admin_register_page' ) ) { return; } $marketplace_pages = self::get_marketplace_pages(); foreach ( $marketplace_pages as $marketplace_page ) { if ( ! is_null( $marketplace_page ) ) { wc_admin_register_page( $marketplace_page ); } } } /** * Get report pages. */ public static function get_marketplace_pages() { $marketplace_pages = array( array( 'id' => 'woocommerce-marketplace', 'parent' => 'woocommerce', 'title' => __( 'Extensions', 'woocommerce' ), 'path' => '/extensions', ), ); /** * The marketplace items used in the menu. * * @since 8.0 */ return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages ); } /** * Enqueue update script. * * @param string $hook_suffix The current admin page. */ public function enqueue_scripts( $hook_suffix ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( 'woocommerce_page_wc-admin' !== $hook_suffix ) { return; }; if ( ! isset( $_GET['path'] ) || '/extensions' !== $_GET['path'] ) { return; } // Enqueue WordPress updates script to enable plugin and theme installs and updates. wp_enqueue_script( 'updates' ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Add a Woo Marketplace link to the plugin install action links. * * @param array $tabs Plugins list tabs. * @return array */ public function add_woo_plugin_install_action_link( $tabs ) { $tabs[ self::MARKETPLACE_TAB_SLUG ] = 'Woo'; return $tabs; } /** * Open the Woo tab when the user clicks on the Woo link in the plugin installer. */ public function maybe_open_woo_tab() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['tab'] ) || self::MARKETPLACE_TAB_SLUG !== $_GET['tab'] ) { return; } // phpcs:enable WordPress.Security.NonceVerification.Recommended $woo_url = add_query_arg( array( 'page' => 'wc-admin', 'path' => '/extensions', 'tab' => 'extensions', 'ref' => 'plugins', ), admin_url( 'admin.php' ) ); wp_safe_redirect( $woo_url ); exit; } /** * Add styles to the plugin install page. */ public function add_plugins_page_styles() { ?> <style> .plugin-install-woo > a::after { content: ""; display: inline-block; background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23646970'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23646970'/%3E%3C/svg%3E%0A"); width: 16px; height: 16px; background-repeat: no-repeat; vertical-align: text-top; margin-left: 2px; } .plugin-install-woo:hover > a::after { background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23135E96'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23135E96'/%3E%3C/svg%3E%0A"); } </style> <?php } } RemoteInboxNotifications.php 0000644 00000001644 15073235522 0012253 0 ustar 00 <?php /** * Remote Inbox Notifications feature. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine; /** * Remote Inbox Notifications feature logic. */ class RemoteInboxNotifications { /** * Option name used to toggle this feature. */ const TOGGLE_OPTION_NAME = 'woocommerce_show_marketplace_suggestions'; /** * Class instance. * * @var RemoteInboxNotifications instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { if ( Features::is_enabled( 'remote-inbox-notifications' ) ) { RemoteInboxNotificationsEngine::init(); } } } Coupons.php 0000644 00000006153 15073235522 0006714 0 ustar 00 <?php /** * WooCommerce Marketing > Coupons. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved; use Automattic\WooCommerce\Admin\PageController; /** * Contains backend logic for the Coupons feature. */ class Coupons { use CouponsMovedTrait; /** * Class instance. * * @var Coupons instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { if ( ! is_admin() ) { return; } // If the main marketing feature is disabled, don't modify coupon behavior. if ( ! Features::is_enabled( 'marketing' ) ) { return; } // Only support coupon modifications if coupons are enabled. if ( ! wc_coupons_enabled() ) { return; } ( new CouponPageMoved() )->init(); add_action( 'admin_enqueue_scripts', array( $this, 'maybe_add_marketing_coupon_script' ) ); add_action( 'woocommerce_register_post_type_shop_coupon', array( $this, 'move_coupons' ) ); add_action( 'admin_head', array( $this, 'fix_coupon_menu_highlight' ), 99 ); add_action( 'admin_menu', array( $this, 'maybe_add_coupon_menu_redirect' ) ); } /** * Maybe add menu item back in original spot to help people transition */ public function maybe_add_coupon_menu_redirect() { if ( ! $this->should_display_legacy_menu() ) { return; } add_submenu_page( 'woocommerce', __( 'Coupons', 'woocommerce' ), __( 'Coupons', 'woocommerce' ), 'manage_options', 'coupons-moved', [ $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 <?php /** * WooCommerce Marketing. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; use Automattic\WooCommerce\Admin\PageController; /** * Contains backend logic for the Marketing feature. */ class Marketing { use CouponsMovedTrait; /** * Class instance. * * @var Marketing instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { if ( ! is_admin() ) { return; } add_action( 'admin_menu', array( $this, 'register_pages' ), 5 ); add_action( 'admin_menu', array( $this, 'add_parent_menu_item' ), 6 ); add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 30 ); } /** * Add main marketing menu item. * * Uses priority of 9 so other items can easily be added at the default priority (10). */ public function add_parent_menu_item() { if ( ! Features::is_enabled( 'navigation' ) ) { add_menu_page( __( 'Marketing', 'woocommerce' ), __( 'Marketing', 'woocommerce' ), 'manage_woocommerce', 'woocommerce-marketing', null, 'dashicons-megaphone', 58 ); } PageController::get_instance()->connect_page( [ 'id' => 'woocommerce-marketing', 'title' => 'Marketing', 'capability' => 'manage_woocommerce', 'path' => 'wc-admin&path=/marketing', ] ); } /** * Registers report pages. */ public function register_pages() { $this->register_overview_page(); $controller = PageController::get_instance(); $defaults = [ 'parent' => 'woocommerce-marketing', 'existing_page' => false, ]; $marketing_pages = apply_filters( 'woocommerce_marketing_menu_items', [] ); foreach ( $marketing_pages as $marketing_page ) { if ( ! is_array( $marketing_page ) ) { continue; } $marketing_page = array_merge( $defaults, $marketing_page ); if ( $marketing_page['existing_page'] ) { $controller->connect_page( $marketing_page ); } else { $controller->register_page( $marketing_page ); } } } /** * Register the main Marketing page, which is Marketing > Overview. * * This is done separately because we need to ensure the page is registered properly and * that the link is done properly. For some reason the normal page registration process * gives us the wrong menu link. */ protected function register_overview_page() { global $submenu; // First register the page. PageController::get_instance()->register_page( [ 'id' => 'woocommerce-marketing-overview', 'title' => __( 'Overview', 'woocommerce' ), 'path' => 'wc-admin&path=/marketing', 'parent' => 'woocommerce-marketing', 'nav_args' => array( 'parent' => 'woocommerce-marketing', 'order' => 10, ), ] ); // Now fix the path, since register_page() gets it wrong. if ( ! isset( $submenu['woocommerce-marketing'] ) ) { return; } foreach ( $submenu['woocommerce-marketing'] as &$item ) { // The "slug" (aka the path) is the third item in the array. if ( 0 === strpos( $item[2], 'wc-admin' ) ) { $item[2] = 'admin.php?page=' . $item[2]; } } } /** * 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 <?php /** * WooCommerce Homescreen. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Shipping; /** * Contains backend logic for the homescreen feature. */ class Homescreen { /** * Menu slug. */ const MENU_SLUG = 'wc-admin'; /** * Class instance. * * @var Homescreen instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) ); add_action( 'admin_menu', array( $this, 'register_page' ) ); // In WC Core 5.1 $submenu manipulation occurs in admin_menu, not admin_head. See https://github.com/woocommerce/woocommerce/pull/29088. if ( version_compare( WC_VERSION, '5.1', '>=' ) ) { // priority is 20 to run after admin_menu hook for woocommerce runs, so that submenu is populated. add_action( 'admin_menu', array( $this, 'possibly_remove_woocommerce_menu' ) ); add_action( 'admin_menu', array( $this, 'update_link_structure' ), 20 ); } else { // priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165. add_action( 'admin_head', array( $this, 'update_link_structure' ), 20 ); } add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) ); if ( Features::is_enabled( 'shipping-smart-defaults' ) ) { add_filter( 'woocommerce_admin_shared_settings', array( $this, 'maybe_set_default_shipping_options_on_home' ), 9999 ); } } /** * Set free shipping in the same country as the store default * Flag rate in all other countries when any of the following conditions are ture * * - The store sells physical products, has JP and WCS installed and connected, and is located in the US. * - The store sells physical products, and is not located in US/Canada/Australia/UK (irrelevant if JP is installed or not). * - The store sells physical products and is located in US, but JP and WCS are not installed. * * @param array $settings shared admin settings. * @return array */ public function maybe_set_default_shipping_options_on_home( $settings ) { if ( ! function_exists( 'get_current_screen' ) ) { return $settings; } $current_screen = get_current_screen(); // Abort if it's not the homescreen. if ( ! isset( $current_screen->id ) || 'woocommerce_page_wc-admin' !== $current_screen->id ) { return $settings; } // Abort if we already created the shipping options. $already_created = get_option( 'woocommerce_admin_created_default_shipping_zones' ); if ( $already_created === 'yes' ) { return $settings; } $zone_count = count( \WC_Data_Store::load( 'shipping-zone' )->get_zones() ); if ( $zone_count ) { update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' ); update_option( 'woocommerce_admin_reviewed_default_shipping_zones', 'yes' ); return $settings; } $user_skipped_obw = $settings['onboarding']['profile']['skipped'] ?? false; $store_address = $settings['preloadSettings']['general']['woocommerce_store_address'] ?? ''; $product_types = $settings['onboarding']['profile']['product_types'] ?? array(); $user_has_set_store_country = $settings['onboarding']['profile']['is_store_country_set'] ?? false; // Do not proceed if user has not filled out their country in the onboarding profiler. if ( ! $user_has_set_store_country ) { return $settings; } // If user skipped the obw or has not completed the store_details // then we assume the user is going to sell physical products. if ( $user_skipped_obw || '' === $store_address ) { $product_types[] = 'physical'; } if ( false === in_array( 'physical', $product_types, true ) ) { return $settings; } $country_code = wc_format_country_state_string( $settings['preloadSettings']['general']['woocommerce_default_country'] )['country']; $country_name = WC()->countries->get_countries()[ $country_code ] ?? null; $is_jetpack_installed = in_array( 'jetpack', $settings['plugins']['installedPlugins'] ?? array(), true ); $is_wcs_installed = in_array( 'woocommerce-services', $settings['plugins']['installedPlugins'] ?? array(), true ); if ( ( 'US' === $country_code && $is_jetpack_installed ) || ( ! in_array( $country_code, array( 'CA', 'AU', 'NZ', 'SG', 'HK', 'GB', 'ES', 'IT', 'DE', 'FR', 'CL', 'AR', 'PE', 'BR', 'UY', 'GT', 'NL', 'AT', 'BE' ), true ) ) || ( 'US' === $country_code && false === $is_jetpack_installed && false === $is_wcs_installed ) ) { $zone = new \WC_Shipping_Zone(); $zone->set_zone_name( $country_name ); $zone->add_location( $country_code, 'country' ); // Method creation has no default title, use the REST API to add a title. $instance_id = $zone->add_shipping_method( 'free_shipping' ); $request = new \WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); $request->set_body_params( array( 'settings' => array( 'title' => 'Free shipping', ), ) ); rest_do_request( $request ); update_option( 'woocommerce_admin_created_default_shipping_zones', 'yes' ); Shipping::delete_zone_count_transient(); } return $settings; } /** * Adds fields so that we can store performance indicators, row settings, and chart type settings for users. * * @param array $user_data_fields User data fields. * @return array */ public function add_user_data_fields( $user_data_fields ) { return array_merge( $user_data_fields, array( 'homepage_layout', 'homepage_stats', 'task_list_tracked_started_tasks', 'help_panel_highlight_shown', ) ); } /** * Registers home page. */ public function register_page() { // Register a top-level item for users who cannot view the core WooCommerce menu. if ( ! self::is_admin_user() ) { wc_admin_register_page( array( 'id' => 'woocommerce-home', 'title' => __( 'WooCommerce', 'woocommerce' ), 'path' => self::MENU_SLUG, 'capability' => 'read', ) ); return; } wc_admin_register_page( array( 'id' => 'woocommerce-home', 'title' => __( 'Home', 'woocommerce' ), 'parent' => 'woocommerce', 'path' => self::MENU_SLUG, 'order' => 0, 'capability' => 'read', ) ); } /** * Check if the user can access the top-level WooCommerce item. * * @return bool */ public static function is_admin_user() { if ( ! class_exists( 'WC_Admin_Menus', false ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin-menus.php'; } if ( method_exists( 'WC_Admin_Menus', 'can_view_woocommerce_menu_item' ) ) { return \WC_Admin_Menus::can_view_woocommerce_menu_item() || current_user_can( 'manage_woocommerce' ); } else { // We leave this line for WC versions <= 6.2. return current_user_can( 'edit_others_shop_orders' ) || current_user_can( 'manage_woocommerce' ); } } /** * Possibly remove the WooCommerce menu item if it was purely used to access wc-admin pages. */ public function possibly_remove_woocommerce_menu() { global $menu; if ( self::is_admin_user() ) { return; } foreach ( $menu as $key => $menu_item ) { if ( self::MENU_SLUG !== $menu_item[2] || 'read' !== $menu_item[1] ) { continue; } unset( $menu[ $key ] ); } } /** * Update the WooCommerce menu structure to make our main dashboard/handler * the top level link for 'WooCommerce'. */ public function update_link_structure() { global $submenu; // User does not have capabilites to see the submenu. if ( ! current_user_can( 'manage_woocommerce' ) || empty( $submenu['woocommerce'] ) ) { return; } $wc_admin_key = null; foreach ( $submenu['woocommerce'] as $submenu_key => $submenu_item ) { if ( self::MENU_SLUG === $submenu_item[2] ) { $wc_admin_key = $submenu_key; break; } } if ( ! $wc_admin_key ) { return; } $menu = $submenu['woocommerce'][ $wc_admin_key ]; // Move menu item to top of array. unset( $submenu['woocommerce'][ $wc_admin_key ] ); array_unshift( $submenu['woocommerce'], $menu ); } /** * Preload options to prime state of the application. * * @param array $options Array of options to preload. * @return array */ public function preload_options( $options ) { $options[] = 'woocommerce_default_homepage_layout'; $options[] = 'woocommerce_admin_install_timestamp'; return $options; } } Notes/OrderMilestones.php 0000644 00000022206 15073235522 0011471 0 ustar 00 <?php /** * WooCommerce Admin (Dashboard) Order Milestones Note Provider. * * Adds a note to the merchant's inbox when certain order milestones are reached. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; /** * Order_Milestones */ class OrderMilestones { /** * Name of the "other milestones" note. */ const NOTE_NAME = 'wc-admin-orders-milestone'; /** * Option key name to store last order milestone. */ const LAST_ORDER_MILESTONE_OPTION_KEY = 'woocommerce_admin_last_orders_milestone'; /** * Hook to process order milestones. */ const PROCESS_ORDERS_MILESTONE_HOOK = 'wc_admin_process_orders_milestone'; /** * Allowed order statuses for calculating milestones. * * @var array */ protected $allowed_statuses = array( 'pending', 'processing', 'completed', ); /** * Orders count cache. * * @var int */ protected $orders_count = null; /** * Further order milestone thresholds. * * @var array */ protected $milestones = array( 1, 10, 100, 250, 500, 1000, 5000, 10000, 500000, 1000000, ); /** * Delay hook attachment until after the WC post types have been registered. * * This is required for retrieving the order count. */ public function __construct() { /** * Filter Order statuses that will count towards milestones. * * @since 3.5.0 * * @param array $allowed_statuses Order statuses that will count towards milestones. */ $this->allowed_statuses = apply_filters( 'woocommerce_admin_order_milestone_statuses', $this->allowed_statuses ); add_action( 'woocommerce_after_register_post_type', array( $this, 'init' ) ); register_deactivation_hook( WC_PLUGIN_FILE, array( $this, 'clear_scheduled_event' ) ); } /** * Hook everything up. */ public function init() { if ( ! wp_next_scheduled( self::PROCESS_ORDERS_MILESTONE_HOOK ) ) { wp_schedule_event( time(), 'hourly', self::PROCESS_ORDERS_MILESTONE_HOOK ); } add_action( 'wc_admin_installed', array( $this, 'backfill_last_milestone' ) ); add_action( self::PROCESS_ORDERS_MILESTONE_HOOK, array( $this, 'possibly_add_note' ) ); } /** * Clear out our hourly milestone hook upon plugin deactivation. */ public function clear_scheduled_event() { wp_clear_scheduled_hook( self::PROCESS_ORDERS_MILESTONE_HOOK ); } /** * Get the total count of orders (in the allowed statuses). * * @param bool $no_cache Optional. Skip cache. * @return int Total orders count. */ public function get_orders_count( $no_cache = false ) { if ( $no_cache || is_null( $this->orders_count ) ) { $status_counts = array_map( 'wc_orders_count', $this->allowed_statuses ); $this->orders_count = array_sum( $status_counts ); } return $this->orders_count; } /** * Backfill the store's current milestone. * * Used to avoid celebrating milestones that were reached before plugin activation. */ public function backfill_last_milestone() { // If the milestone notes have been disabled via filter, bail. if ( ! $this->are_milestones_enabled() ) { return; } $this->set_last_milestone( $this->get_current_milestone() ); } /** * Get the store's last milestone. * * @return int Last milestone reached. */ public function get_last_milestone() { return get_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, 0 ); } /** * Update the last reached milestone. * * @param int $milestone Last milestone reached. */ public function set_last_milestone( $milestone ) { update_option( self::LAST_ORDER_MILESTONE_OPTION_KEY, $milestone ); } /** * Calculate the current orders milestone. * * Based on the threshold values in $this->milestones. * * @return int Current orders milestone. */ public function get_current_milestone() { $milestone_reached = 0; $orders_count = $this->get_orders_count(); foreach ( $this->milestones as $milestone ) { if ( $milestone <= $orders_count ) { $milestone_reached = $milestone; } } return $milestone_reached; } /** * Get the appropriate note title for a given milestone. * * @param int $milestone Order milestone. * @return string Note title for the milestone. */ public static function get_note_title_for_milestone( $milestone ) { switch ( $milestone ) { case 1: return __( 'First order received', 'woocommerce' ); case 10: case 100: case 250: case 500: case 1000: case 5000: case 10000: case 500000: case 1000000: return sprintf( /* translators: Number of orders processed. */ __( 'Congratulations on processing %s orders!', 'woocommerce' ), wc_format_decimal( $milestone ) ); default: return ''; } } /** * Get the appropriate note content for a given milestone. * * @param int $milestone Order milestone. * @return string Note content for the milestone. */ public static function get_note_content_for_milestone( $milestone ) { switch ( $milestone ) { case 1: return __( 'Congratulations on getting your first order! Now is a great time to learn how to manage your orders.', 'woocommerce' ); case 10: return __( "You've hit the 10 orders milestone! Look at you go. Browse some WooCommerce success stories for inspiration.", 'woocommerce' ); case 100: case 250: case 500: case 1000: case 5000: case 10000: case 500000: case 1000000: return __( 'Another order milestone! Take a look at your Orders Report to review your orders to date.', 'woocommerce' ); default: return ''; } } /** * Get the appropriate note action for a given milestone. * * @param int $milestone Order milestone. * @return array Note actoion (name, label, query) for the milestone. */ public static function get_note_action_for_milestone( $milestone ) { switch ( $milestone ) { case 1: return array( 'name' => 'learn-more', 'label' => __( 'Learn more', 'woocommerce' ), 'query' => 'https://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 <?php /** * WooCommerce Admin (Dashboard) Giving feedback notes provider * * Adds notes to the merchant's inbox about giving feedback. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\Survey; /** * Giving_Feedback_Notes */ class GivingFeedbackNotes { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-store-notice-giving-feedback-2'; /** * Get the note. * * @return Note */ public static function get_note() { if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) { return; } // Otherwise, create our new note. $note = new Note(); $note->set_title( __( 'You\'re invited to share your experience', 'woocommerce' ) ); $note->set_content( __( 'Now that you’ve chosen us as a partner, our goal is to make sure we\'re providing the right tools to meet your needs. We\'re looking forward to having your feedback on the store setup experience so we can improve it in the future.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'share-feedback', __( 'Share feedback', 'woocommerce' ), Survey::get_url( '/store-setup-survey' ) ); return $note; } } Notes/MerchantEmailNotifications.php 0000644 00000006601 15073235522 0013617 0 ustar 00 <?php /** * Handles merchant email notifications */ namespace Automattic\WooCommerce\Internal\Admin\Notes; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; defined( 'ABSPATH' ) || exit; /** * Merchant email notifications. * This gets all non-sent notes type `email` and sends them. */ class MerchantEmailNotifications { /** * Initialize the merchant email notifications. */ public static function init() { add_action( 'admin_init', array( __CLASS__, 'trigger_notification_action' ) ); } /** * Trigger the note action. */ public static function trigger_notification_action() { /* phpcs:disable WordPress.Security.NonceVerification */ if ( ! isset( $_GET['external_redirect'] ) || 1 !== intval( $_GET['external_redirect'] ) || ! isset( $_GET['user'] ) || ! isset( $_GET['note'] ) || ! isset( $_GET['action'] ) ) { return; } $note_id = intval( $_GET['note'] ); $action_id = intval( $_GET['action'] ); $user_id = intval( $_GET['user'] ); /* phpcs:enable */ $note = Notes::get_note( $note_id ); if ( ! $note || Note::E_WC_ADMIN_NOTE_EMAIL !== $note->get_type() ) { return; } $triggered_action = Notes::get_action_by_id( $note, $action_id ); if ( ! $triggered_action ) { return; } Notes::trigger_note_action( $note, $triggered_action ); $url = $triggered_action->query; // We will use "wp_safe_redirect" when it's an internal redirect. if ( strpos( $url, 'http' ) === false ) { wp_safe_redirect( $url ); } else { header( 'Location: ' . $url ); } exit(); } /** * Send all the notifications type `email`. */ public static function run() { $data_store = Notes::load_data_store(); $notes = $data_store->get_notes( array( 'type' => array( Note::E_WC_ADMIN_NOTE_EMAIL ), 'status' => array( 'unactioned' ), ) ); foreach ( $notes as $note ) { $note = Notes::get_note( $note->note_id ); if ( $note ) { self::send_merchant_notification( $note ); $note->set_status( 'sent' ); $note->save(); } } } /** * Send the notification to the merchant. * * @param object $note The note to send. */ public static function send_merchant_notification( $note ) { \WC_Emails::instance(); $users = self::get_notification_recipients( $note ); $email = new EmailNotification( $note ); foreach ( $users as $user ) { if ( is_email( $user->user_email ) ) { $name = self::get_merchant_preferred_name( $user ); $email->trigger( $user->user_email, $user->ID, $name ); } } } /** * Get the preferred name for user. First choice is * the user's first name, and then display_name. * * @param WP_User $user Recipient to send the note to. * @return string User's name. */ public static function get_merchant_preferred_name( $user ) { $first_name = get_user_meta( $user->ID, 'first_name', true ); if ( $first_name ) { return $first_name; } if ( $user->display_name ) { return $user->display_name; } return ''; } /** * Get users by role to notify. * * @param object $note The note to send. * @return array Users to notify */ public static function get_notification_recipients( $note ) { $content_data = $note->get_content_data(); $role = 'administrator'; if ( isset( $content_data->role ) ) { $role = $content_data->role; } $args = array( 'role' => $role ); return get_users( $args ); } } Notes/FirstProduct.php 0000644 00000004175 15073235522 0011010 0 ustar 00 <?php /** * WooCommerce Admin: Do you need help with adding your first product? * * Adds a note to ask the client if they need help adding their first product. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * First_Product. */ class FirstProduct { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-first-product'; /** * Get the note. * * @return Note */ public static function get_note() { // We want to show the note after seven days. if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) { return; } $onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() ); // Confirm that $onboarding_profile is set. if ( empty( $onboarding_profile ) ) { return; } // Make sure that the person who filled out the OBW was not setting up // the store for their customer/client. if ( ! isset( $onboarding_profile['setup_client'] ) || $onboarding_profile['setup_client'] ) { return; } // Don't show if there are products. $query = new \WC_Product_Query( array( 'limit' => 1, 'paginate' => true, 'return' => 'ids', 'status' => array( 'publish' ), ) ); $products = $query->get_products(); $count = $products->total; if ( 0 !== $count ) { return; } $note = new Note(); $note->set_title( __( 'Do you need help with adding your first product?', 'woocommerce' ) ); $note->set_content( __( 'This video tutorial will help you go through the process of adding your first product in WooCommerce.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'first-product-watch-tutorial', __( 'Watch tutorial', 'woocommerce' ), 'https://www.youtube.com/watch?v=sFtXa00Jf_o&list=PLHdG8zvZd0E575Ia8Mu3w1h750YLXNfsC&index=24' ); return $note; } } Notes/CustomizingProductCatalog.php 0000644 00000004205 15073235522 0013521 0 ustar 00 <?php /** * WooCommerce Admin: How to customize your product catalog note provider * * Adds a note with a link to the customizer a day after adding the first product */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Class CustomizingProductCatalog * * @package Automattic\WooCommerce\Admin\Notes */ class CustomizingProductCatalog { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-customizing-product-catalog'; /** * Get the note. * * @return Note */ public static function get_note() { $query = new \WC_Product_Query( array( 'limit' => 1, 'paginate' => true, 'status' => array( 'publish' ), 'orderby' => 'post_date', 'order' => 'DESC', ) ); $products = $query->get_products(); // we need at least 1 product. if ( 0 === $products->total ) { return; } $product = $products->products[0]; $created_timestamp = $product->get_date_created()->getTimestamp(); $is_a_day_old = ( time() - $created_timestamp ) >= DAY_IN_SECONDS; // the product must be at least 1 day old. if ( ! $is_a_day_old ) { return; } // store must not been active more than 14 days. if ( self::wc_admin_active_for( DAY_IN_SECONDS * 14 ) ) { return; } $note = new Note(); $note->set_title( __( 'How to customize your product catalog', 'woocommerce' ) ); $note->set_content( __( 'You want your product catalog and images to look great and align with your brand. This guide will give you all the tips you need to get your products looking great in your store.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'day-after-first-product', __( 'Learn more', 'woocommerce' ), 'https://woo.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product' ); return $note; } } Notes/LaunchChecklist.php 0000644 00000003260 15073235522 0011416 0 ustar 00 <?php /** * WooCommerce Admin Launch Checklist Note. * * Adds a note to cover pre-launch checklist items for store owners. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Launch_Checklist */ class LaunchChecklist { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-launch-checklist'; /** * Get the note. * * @return Note */ public static function get_note() { // Only add this note if completing the task list or completed 3 tasks in 10 days. $completed_tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() ); $ten_days_in_seconds = 10 * DAY_IN_SECONDS; if ( ! get_option( 'woocommerce_task_list_complete' ) && ( count( $completed_tasks ) < 3 || self::is_wc_admin_active_in_date_range( 'week-1-4', $ten_days_in_seconds ) ) ) { return; } $content = __( 'To make sure you never get that sinking "what did I forget" feeling, we\'ve put together the essential pre-launch checklist.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Ready to launch your store?', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin: WooCommerce Subscriptions. * * Adds a note to learn more about WooCommerce Subscriptions. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; /** * WooCommerce_Subscriptions. */ class WooCommerceSubscriptions { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-woocommerce-subscriptions'; /** * Get the note. * * @return Note|null */ public static function get_note() { $onboarding_data = get_option( OnboardingProfile::DATA_OPTION, array() ); if ( ! isset( $onboarding_data['product_types'] ) || ! in_array( 'subscriptions', $onboarding_data['product_types'], true ) ) { return; } if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) { return; } $note = new Note(); $note->set_title( __( 'Do you need more info about WooCommerce Subscriptions?', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce Subscriptions allows you to introduce a variety of subscriptions for physical or virtual products and services. Create product-of-the-month clubs, weekly service subscriptions or even yearly software billing packages. Add sign-up fees, offer free trials, or set expiration periods.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn More', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin WooCommerce Payments Note Provider. * * Adds a note to the merchant's inbox showing the benefits of the WooCommerce Payments. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * WooCommerce_Payments */ class WooCommercePayments { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-woocommerce-payments'; /** * Name of the note for use in the database. */ const PLUGIN_SLUG = 'woocommerce-payments'; /** * Name of the note for use in the database. */ const PLUGIN_FILE = 'woocommerce-payments/woocommerce-payments.php'; /** * Attach hooks. */ public function __construct() { add_action( 'init', array( $this, 'install_on_action' ) ); add_action( 'wc-admin-woocommerce-payments_add_note', array( $this, 'add_note' ) ); } /** * Maybe add a note on WooCommerce Payments for US based sites older than a week without the plugin installed. */ public static function possibly_add_note() { if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) || 'US' !== WC()->countries->get_base_country() ) { return; } $data_store = Notes::load_data_store(); // We already have this note? Then mark the note as actioned. $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_ids ) ) { $note_id = array_pop( $note_ids ); $note = Notes::get_note( $note_id ); if ( false === $note ) { return; } // If the WooCommerce Payments plugin was installed after the note was created, make sure it's marked as actioned. if ( self::is_installed() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) { $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } return; } $current_date = new \DateTime(); $publish_date = new \DateTime( '2020-04-14' ); if ( $current_date >= $publish_date ) { $note = self::get_note(); if ( self::can_be_added() ) { $note->save(); } return; } else { $hook_name = sprintf( '%s_add_note', self::NOTE_NAME ); if ( ! WC()->queue()->get_next( $hook_name ) ) { WC()->queue()->schedule_single( $publish_date->getTimestamp(), $hook_name ); } } } /** * Add a note about WooCommerce Payments. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Try the new way to get paid', 'woocommerce' ) ); $note->set_content( __( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooPayments</strong>.', 'woocommerce' ) . '<br><br>' . sprintf( /* translators: 1: opening link tag, 2: closing tag */ __( 'By clicking "Get started", you agree to our %1$sTerms of Service%2$s', 'woocommerce' ), '<a href="https://wordpress.com/tos/" target="_blank">', '</a>' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin Usage Tracking Opt In Note Provider. * * Adds a Usage Tracking Opt In extension note. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Tracking_Opt_In */ class TrackingOptIn { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-usage-tracking-opt-in'; /** * Attach hooks. */ public function __construct() { add_action( 'woocommerce_note_action_tracking-opt-in', array( $this, 'opt_in_to_tracking' ) ); } /** * Get the note. * * @return Note|null */ public static function get_note() { // Only show this note to stores that are opted out. if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { return; } // We want to show the note after one week. if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4' ) ) { return; } /* translators: 1: open link to Woo.com settings, 2: open link to Woo.com tracking documentation, 3: close link tag. */ $content_format = __( 'Gathering usage data allows us to improve WooCommerce. Your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense. You can always visit the %1$sSettings%3$s and choose to stop sharing data. %2$sRead more%3$s about what data we collect.', 'woocommerce' ); $note_content = sprintf( $content_format, '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ) ) . '" target="_blank">', '<a href="https://woo.com/usage-tracking?utm_medium=product" target="_blank">', '</a>' ); $note = new Note(); $note->set_title( __( 'Help WooCommerce improve with usage tracking', 'woocommerce' ) ); $note->set_content( $note_content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'tracking-opt-in', __( 'Activate usage tracking', 'woocommerce' ), false, Note::E_WC_ADMIN_NOTE_ACTIONED, true ); return $note; } /** * Opt in to usage tracking when note is actioned. * * @param Note $note Note being acted upon. */ public function opt_in_to_tracking( $note ) { if ( self::NOTE_NAME === $note->get_name() ) { // Opt in to tracking and schedule the first data update. // Same mechanism as in WC_Admin_Setup_Wizard::wc_setup_store_setup_save(). update_option( 'woocommerce_allow_tracking', 'yes' ); wp_schedule_single_event( time() + 10, 'woocommerce_tracker_send_event', array( true ) ); } } } Notes/MarketingJetpack.php 0000644 00000007255 15073235522 0011605 0 ustar 00 <?php /** * WooCommerce Admin Jetpack Marketing Note Provider. * * Adds notes to the merchant's inbox concerning Jetpack Backup. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Admin\PluginsHelper; /** * Suggest Jetpack Backup to Woo users. * * Note: This should probably live in the Jetpack plugin in the future. * * @see https://developer.woo.com/2020/10/16/using-the-admin-notes-inbox-in-woocommerce/ */ class MarketingJetpack { // Shared Note Traits. use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-marketing-jetpack-backup'; /** * Product IDs that include Backup. */ const BACKUP_IDS = [ 2010, 2011, 2012, 2013, 2014, 2015, 2100, 2101, 2102, 2103, 2005, 2006, 2000, 2003, 2001, 2004, ]; /** * Maybe add a note on Jetpack Backups for Jetpack sites older than a week without Backups. */ public static function possibly_add_note() { /** * Check if Jetpack is installed. */ $installed_plugins = PluginsHelper::get_installed_plugin_slugs(); if ( ! in_array( 'jetpack', $installed_plugins, true ) ) { return; } $data_store = \WC_Data_Store::load( 'admin-note' ); // Do we already have this note? $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_ids ) ) { $note_id = array_pop( $note_ids ); $note = Notes::get_note( $note_id ); if ( false === $note ) { return; } // If Jetpack Backups was purchased after the note was created, mark this note as actioned. if ( self::has_backups() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) { $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } return; } // Check requirements. if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', DAY_IN_SECONDS * 3 ) || ! self::can_be_added() || self::has_backups() ) { return; } // Add note. $note = self::get_note(); $note->save(); } /** * Get the note. */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Protect your WooCommerce Store with Jetpack Backup.', 'woocommerce' ) ); $note->set_content( __( 'Store downtime means lost sales. One-click restores get you back online quickly if something goes wrong.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_layout( 'thumbnail' ); $note->set_image( WC_ADMIN_IMAGES_FOLDER_URL . '/admin_notes/marketing-jetpack-2x.png' ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin-notes' ); $note->add_action( 'jetpack-backup-woocommerce', __( 'Get backups', 'woocommerce' ), esc_url( 'https://jetpack.com/upgrade/backup-woocommerce/?utm_source=inbox&utm_medium=automattic_referred&utm_campaign=jp_backup_to_woo' ), Note::E_WC_ADMIN_NOTE_ACTIONED ); return $note; } /** * Check if this blog already has a Jetpack Backups product. * * @return boolean Whether or not this blog has backups. */ protected static function has_backups() { $product_ids = []; $plan = get_option( 'jetpack_active_plan' ); if ( ! empty( $plan ) ) { $product_ids[] = $plan['product_id']; } $products = get_option( 'jetpack_site_products' ); if ( ! empty( $products ) ) { foreach ( $products as $product ) { $product_ids[] = $product['product_id']; } } return (bool) array_intersect( self::BACKUP_IDS, $product_ids ); } } Notes/PaymentsRemindMeLater.php 0000644 00000003676 15073235522 0012576 0 ustar 00 <?php /** * WooCommerce Admin Payment Reminder Me later */ namespace Automattic\WooCommerce\Internal\Admin\Notes; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage; defined( 'ABSPATH' ) || exit; /** * PaymentsRemindMeLater */ class PaymentsRemindMeLater { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-payments-remind-me-later'; /** * Should this note exist? */ public static function is_applicable() { return self::should_display_note(); } /** * Returns true if we should display the note. * * @return bool */ public static function should_display_note() { // WCPay welcome page must be visible. if ( ! WcPayWelcomePage::instance()->must_be_visible() ) { return false; } // Less than 3 days since viewing welcome page. $view_timestamp = get_option( 'wcpay_welcome_page_viewed_timestamp', false ); if ( ! $view_timestamp || ( time() - $view_timestamp < 3 * DAY_IN_SECONDS ) ) { return false; } return true; } /** * Get the note. * * @return Note */ public static function get_note() { if ( ! self::should_display_note() ) { return; } $content = __( 'Save up to $800 in fees by managing transactions with WooPayments. With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Save big with WooPayments', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' ) ); return $note; } } Notes/PerformanceOnMobile.php 0000644 00000003205 15073235522 0012237 0 ustar 00 <?php /** * WooCommerce Admin Performance on mobile note. * * Adds a note to download the mobile app, performance on mobile. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Performance_On_Mobile */ class PerformanceOnMobile { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-performance-on-mobile'; /** * Get the note. * * @return Note */ public static function get_note() { // Only add this note if this store is at least 9 months old. $nine_months_in_seconds = MONTH_IN_SECONDS * 9; if ( ! self::wc_admin_active_for( $nine_months_in_seconds ) ) { return; } // Check that the previous mobile app notes have not been actioned. if ( MobileApp::has_note_been_actioned() ) { return; } if ( RealTimeOrderAlerts::has_note_been_actioned() ) { return; } if ( ManageOrdersOnTheGo::has_note_been_actioned() ) { return; } $note = new Note(); $note->set_title( __( 'Track your store performance on mobile', 'woocommerce' ) ); $note->set_content( __( 'Monitor your sales and high performing products with the Woo app.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_source=inbox&utm_medium=product' ); return $note; } } Notes/PaymentsMoreInfoNeeded.php 0000644 00000004000 15073235522 0012707 0 ustar 00 <?php /** * WooCommerce Admin Payments More Info Needed Inbox Note Provider */ namespace Automattic\WooCommerce\Internal\Admin\Notes; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage; defined( 'ABSPATH' ) || exit; /** * PaymentsMoreInfoNeeded */ class PaymentsMoreInfoNeeded { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-payments-more-info-needed'; /** * Should this note exist? */ public static function is_applicable() { return self::should_display_note(); } /** * Returns true if we should display the note. * * @return bool */ public static function should_display_note() { // WCPay welcome page must not be visible. if ( WcPayWelcomePage::instance()->must_be_visible() ) { return false; } // More than 30 days since viewing the welcome page. $exit_survey_timestamp = get_option( 'wcpay_welcome_page_exit_survey_more_info_needed_timestamp', false ); if ( ! $exit_survey_timestamp || ( time() - $exit_survey_timestamp < 30 * DAY_IN_SECONDS ) ) { return false; } return true; } /** * Get the note. * * @return Note */ public static function get_note() { if ( ! self::should_display_note() ) { return; } $content = __( 'We recently asked you if you wanted more information about WooPayments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Payments made simple with WooPayments', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more here', 'woocommerce' ), 'https://woo.com/payments/' ); return $note; } } Notes/WooSubscriptionsNotes.php 0000644 00000034346 15073235522 0012730 0 ustar 00 <?php /** * WooCommerce Admin (Dashboard) Woo.com Extension Subscriptions Note Provider. * * Adds notes to the merchant's inbox concerning Woo.com extension subscriptions. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\PageController; /** * Woo_Subscriptions_Notes */ class WooSubscriptionsNotes { const LAST_REFRESH_OPTION_KEY = 'woocommerce_admin-wc-helper-last-refresh'; const NOTE_NAME = 'wc-admin-wc-helper-connection'; const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection'; const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription'; const NOTIFY_WHEN_DAYS_LEFT = 60; /** * We want to bubble up expiration notices when they cross certain age * thresholds. PHP 5.2 doesn't support constant arrays, so we do this. * * @return array */ private function get_bump_thresholds() { return array( 60, 45, 20, 7, 1 ); // days. } /** * Hook all the things. */ public function __construct() { add_action( 'admin_head', array( $this, 'admin_head' ) ); add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 ); } /** * Reacts to changes in the helper option. * * @param array $old_value The previous value of the option. * @param array $value The new value of the option. */ public function update_option_woocommerce_helper_data( $old_value, $value ) { if ( ! is_array( $old_value ) ) { $old_value = array(); } if ( ! is_array( $value ) ) { $value = array(); } $old_auth = array_key_exists( 'auth', $old_value ) ? $old_value['auth'] : array(); $new_auth = array_key_exists( 'auth', $value ) ? $value['auth'] : array(); $old_token = array_key_exists( 'access_token', $old_auth ) ? $old_auth['access_token'] : ''; $new_token = array_key_exists( 'access_token', $new_auth ) ? $new_auth['access_token'] : ''; // The site just disconnected. if ( ! empty( $old_token ) && empty( $new_token ) ) { $this->remove_notes(); $this->add_no_connection_note(); return; } // The site is connected. if ( $this->is_connected() ) { $this->remove_notes(); $this->refresh_subscription_notes(); return; } } /** * Runs on `admin_head` hook. Checks the connection and refreshes subscription notes on relevant pages. */ public function admin_head() { if ( ! PageController::is_admin_or_embed_page() ) { // To avoid unnecessarily calling Helper API, we only want to refresh subscription notes, // if the request is initiated from the wc admin dashboard or a WC related page which includes // the Activity button in WC header. return; } $this->check_connection(); if ( $this->is_connected() ) { $refresh_notes = false; // Did the user just do something on the helper page?. if ( isset( $_GET['wc-helper-status'] ) ) { // @codingStandardsIgnoreLine. $refresh_notes = true; } // Has it been more than a day since we last checked? // Note: We do it this way and not wp_scheduled_task since WC_Helper_Options is not loaded for cron. $time_now_gmt = current_time( 'timestamp', 0 ); $last_refresh = intval( get_option( self::LAST_REFRESH_OPTION_KEY, 0 ) ); if ( $last_refresh + DAY_IN_SECONDS <= $time_now_gmt ) { update_option( self::LAST_REFRESH_OPTION_KEY, $time_now_gmt ); $refresh_notes = true; } if ( $refresh_notes ) { $this->refresh_subscription_notes(); } } } /** * Checks the connection. Adds a note (as necessary) if there is no connection. */ public function check_connection() { if ( ! $this->is_connected() ) { $data_store = Notes::load_data_store(); $note_ids = $data_store->get_notes_with_name( self::CONNECTION_NOTE_NAME ); if ( ! empty( $note_ids ) ) { // We already have a connection note. Exit early. return; } $this->remove_notes(); $this->add_no_connection_note(); } } /** * Whether or not we think the site is currently connected to 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 <?php /** * WooCommerce Admin (Dashboard) New Sales Record Note Provider. * * Adds a note to the merchant's inbox when the previous day's sales are a new record. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * New_Sales_Record */ class NewSalesRecord { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-new-sales-record'; /** * Option name for the sales record date in ISO 8601 (YYYY-MM-DD) date. */ const RECORD_DATE_OPTION_KEY = 'woocommerce_sales_record_date'; /** * Option name for the sales record amount. */ const RECORD_AMOUNT_OPTION_KEY = 'woocommerce_sales_record_amount'; /** * Returns the total of yesterday's sales. * * @param string $date Date for sales to sum (i.e. YYYY-MM-DD). * @return floatval */ public static function sum_sales_for_date( $date ) { $order_query = new \WC_Order_Query( array( 'date_created' => $date ) ); $orders = $order_query->get_orders(); $total = 0; foreach ( (array) $orders as $order ) { $total += $order->get_total(); } return $total; } /** * Possibly add a sales record note. */ public static function possibly_add_note() { /** * Filter to allow for disabling sales record milestones. * * @since 3.7.0 * * @param boolean default true */ $sales_record_notes_enabled = apply_filters( 'woocommerce_admin_sales_record_milestone_enabled', true ); if ( ! $sales_record_notes_enabled ) { return; } $yesterday = gmdate( 'Y-m-d', current_time( 'timestamp', 0 ) - DAY_IN_SECONDS ); $total = self::sum_sales_for_date( $yesterday ); // No sales yesterday? Bail. if ( 0 >= $total ) { return; } $record_date = get_option( self::RECORD_DATE_OPTION_KEY, '' ); $record_amt = floatval( get_option( self::RECORD_AMOUNT_OPTION_KEY, 0 ) ); // No previous entry? Just enter what we have and return without generating a note. if ( empty( $record_date ) ) { update_option( self::RECORD_DATE_OPTION_KEY, $yesterday ); update_option( self::RECORD_AMOUNT_OPTION_KEY, $total ); return; } // Otherwise, if yesterdays total bested the record, update AND generate a note. if ( $total > $record_amt ) { update_option( self::RECORD_DATE_OPTION_KEY, $yesterday ); update_option( self::RECORD_AMOUNT_OPTION_KEY, $total ); // We only want one sales record note at any time in the inbox, so we delete any other first. Notes::delete_notes_with_name( self::NOTE_NAME ); $note = self::get_note_with_record_data( $record_date, $record_amt, $yesterday, $total ); $note->save(); } } /** * Get the note with record data. * * @param string $record_date record date Y-m-d. * @param float $record_amt record amount. * @param string $yesterday yesterday's date Y-m-d. * @param string $total total sales for yesterday. * * @return Note */ public static function get_note_with_record_data( $record_date, $record_amt, $yesterday, $total ) { // Use F jS (March 7th) format for English speaking countries. if ( substr( get_user_locale(), 0, 2 ) === 'en' ) { $date_format = 'F jS'; } else { // otherwise, fallback to the system date format. $date_format = get_option( 'date_format' ); } $formatted_yesterday = date_i18n( $date_format, strtotime( $yesterday ) ); $formatted_total = html_entity_decode( wp_strip_all_tags( wc_price( $total ) ) ); $formatted_record_date = date_i18n( $date_format, strtotime( $record_date ) ); $formatted_record_amt = html_entity_decode( wp_strip_all_tags( wc_price( $record_amt ) ) ); $content = sprintf( /* translators: 1 and 4: Date (e.g. October 16th), 2 and 3: Amount (e.g. $160.00) */ __( 'Woohoo, %1$s was your record day for sales! Net sales was %2$s beating the previous record of %3$s set on %4$s.', 'woocommerce' ), $formatted_yesterday, $formatted_total, $formatted_record_amt, $formatted_record_date ); $content_data = (object) array( 'old_record_date' => $record_date, 'old_record_amt' => $record_amt, 'new_record_date' => $yesterday, 'new_record_amt' => $total, ); $report_url = '?page=wc-admin&path=/analytics/revenue&period=custom&compare=previous_year&after=' . $yesterday . '&before=' . $yesterday; // And now, create our new note. $note = new Note(); $note->set_title( __( 'New sales record!', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( $content_data ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'view-report', __( 'View report', 'woocommerce' ), $report_url ); return $note; } /** * Get the note. This is used for localizing the note. * * @return Note */ public static function get_note() { $note = Notes::get_note_by_name( self::NOTE_NAME ); if ( ! $note ) { return false; } $content_data = $note->get_content_data(); return self::get_note_with_record_data( $content_data->old_record_date, $content_data->old_record_amt, $content_data->new_record_date, $content_data->new_record_amt ); } } Notes/InstallJPAndWCSPlugins.php 0000644 00000011122 15073235522 0012550 0 ustar 00 <?php /** * WooCommerce Admin Add Install Jetpack and WooCommerce Shipping & Tax Plugin Note Provider. * * Adds a note to the merchant's inbox prompting them to install the Jetpack * and WooCommerce Shipping & Tax plugins after it fails to install during * WooCommerce setup. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Admin\PluginsHelper; /** * Install_JP_And_WCS_Plugins */ class InstallJPAndWCSPlugins { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-install-jp-and-wcs-plugins'; /** * Constructor. */ public function __construct() { add_action( 'woocommerce_note_action_install-jp-and-wcs-plugins', array( $this, 'install_jp_and_wcs_plugins' ) ); add_action( 'activated_plugin', array( $this, 'action_note' ) ); add_action( 'woocommerce_plugins_install_api_error', array( $this, 'on_install_error' ) ); add_action( 'woocommerce_plugins_install_error', array( $this, 'on_install_error' ) ); add_action( 'woocommerce_plugins_activate_error', array( $this, 'on_install_error' ) ); } /** * Get the note. * * @return Note */ public static function get_note() { $content = __( 'We noticed that there was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again and enjoy all the advantages of having the plugins connected to your store! Sorry for the inconvenience. The "Jetpack" and "WooCommerce Shipping & Tax" plugins will be installed & activated for free.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Uh oh... There was a problem during the Jetpack and WooCommerce Shipping & Tax install. Please try again.', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'install-jp-and-wcs-plugins', __( 'Install plugins', 'woocommerce' ), false, Note::E_WC_ADMIN_NOTE_ACTIONED ); return $note; } /** * Action the Install Jetpack and WooCommerce Shipping & Tax note, if any exists, * and as long as both the Jetpack and WooCommerce Shipping & Tax plugins have been * activated. */ public static function action_note() { // Make sure that both plugins are active before actioning the note. $active_plugin_slugs = PluginsHelper::get_active_plugin_slugs(); $jp_active = in_array( 'jetpack', $active_plugin_slugs, true ); $wcs_active = in_array( 'woocommerce-services', $active_plugin_slugs, true ); if ( ! $jp_active || ! $wcs_active ) { return; } // Action any notes with a matching name. $data_store = Notes::load_data_store(); $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); foreach ( $note_ids as $note_id ) { $note = Notes::get_note( $note_id ); if ( $note ) { $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } } } /** * Install the Jetpack and WooCommerce Shipping & Tax plugins in response to the action * being clicked in the admin note. * * @param Note $note The note being actioned. */ public function install_jp_and_wcs_plugins( $note ) { if ( self::NOTE_NAME !== $note->get_name() ) { return; } $this->install_and_activate_plugin( 'jetpack' ); $this->install_and_activate_plugin( 'woocommerce-services' ); } /** * Installs and activates the specified plugin. * * @param string $plugin The plugin slug. */ private function install_and_activate_plugin( $plugin ) { $install_request = array( 'plugin' => $plugin ); $installer = new \Automattic\WooCommerce\Admin\API\OnboardingPlugins(); $result = $installer->install_plugin( $install_request ); // @todo Use the error statuses to decide whether or not to action the note. if ( is_wp_error( $result ) ) { return; } $activate_request = array( 'plugins' => $plugin ); $installer->activate_plugins( $activate_request ); } /** * Create an alert notification in response to an error installing a plugin. * * @param string $slug The slug of the plugin being installed. */ public function on_install_error( $slug ) { // Exit early if we're not installing the Jetpack or the WooCommerce Shipping & Tax plugins. if ( 'jetpack' !== $slug && 'woocommerce-services' !== $slug ) { return; } self::possibly_add_note(); } } Notes/EUVATNumber.php 0000644 00000003202 15073235522 0010403 0 ustar 00 <?php /** * WooCommerce Admin: EU VAT Number Note. * * Adds a note for EU store to install the EU VAT Number extension. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * EU_VAT_Number */ class EUVATNumber { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-eu-vat-number'; /** * Get the note. * * @return Note */ public static function get_note() { if ( 'yes' !== get_option( 'wc_connect_taxes_enabled', 'no' ) ) { return; } $country_code = WC()->countries->get_base_country(); $eu_countries = WC()->countries->get_european_union_countries(); if ( ! in_array( $country_code, $eu_countries, true ) ) { return; } $content = __( "If your store is based in the EU, we recommend using the EU VAT Number extension in addition to automated taxes. It provides your checkout with a field to collect and validate a customer's EU VAT number, if they have one.", 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Collect and validate EU VAT numbers at checkout', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin: Migrate from Shopify to WooCommerce. * * Adds a note to ask the client if they want to migrate from Shopify to WooCommerce. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Migrate_From_Shopify. */ class MigrateFromShopify { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-migrate-from-shopify'; /** * Get the note. * * @return Note */ public static function get_note() { // We want to show the note after two days. $two_days = 2 * DAY_IN_SECONDS; if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days ) ) { return; } $onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() ); if ( ! isset( $onboarding_profile['setup_client'] ) || ! isset( $onboarding_profile['selling_venues'] ) || ! isset( $onboarding_profile['other_platform'] ) ) { return; } // Make sure the client is not setup. if ( $onboarding_profile['setup_client'] ) { return; } // We will show the notification when the client already is selling and is using Shopify. if ( 'other' !== $onboarding_profile['selling_venues'] || 'shopify' !== $onboarding_profile['other_platform'] ) { return; } $note = new Note(); $note->set_title( __( 'Do you want to migrate from Shopify to WooCommerce?', 'woocommerce' ) ); $note->set_content( __( 'Changing eCommerce platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'migrate-from-shopify', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin: Selling Online Courses note * * Adds a note to encourage selling online courses. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; /** * Selling_Online_Courses */ class SellingOnlineCourses { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-selling-online-courses'; /** * Attach hooks. */ public function __construct() { add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( $this, 'check_onboarding_profile' ), 10, 3 ); } /** * Check to see if the profiler options match before possibly adding note. * * @param object $old_value The old option value. * @param object $value The new option value. * @param string $option The name of the option. */ public static function check_onboarding_profile( $old_value, $value, $option ) { // Skip adding if this store is in the education/learning industry. if ( ! isset( $value['industry'] ) ) { return; } $industry_slugs = array_column( $value['industry'], 'slug' ); if ( ! in_array( 'education-and-learning', $industry_slugs, true ) ) { return; } self::possibly_add_note(); } /** * Get the note. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Do you want to sell online courses?', 'woocommerce' ) ); $note->set_content( __( 'Online courses are a great solution for any business that can teach a new skill. Since courses don’t require physical product development or shipping, they’re affordable, fast to create, and can generate passive income for years to come. In this article, we provide you more information about selling courses using WooCommerce.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin Unsecured Files Note. * * Adds a warning about potentially unsecured files. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; if ( ! class_exists( Note::class ) ) { class_alias( WC_Admin_Note::class, Note::class ); } /** * Unsecured_Report_Files */ class UnsecuredReportFiles { /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-remove-unsecured-report-files'; /** * Get the note. * * @return Note|null */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Potentially unsecured files were found in your uploads directory', 'woocommerce' ) ); $note->set_content( sprintf( /* translators: 1: opening analytics docs link tag. 2: closing link tag */ __( 'Files that may contain %1$sstore analytics%2$s reports were found in your uploads directory - we recommend assessing and deleting any such files.', 'woocommerce' ), '<a href="https://woo.com/document/woocommerce-analytics/" target="_blank">', '</a>' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_ERROR ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://developer.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 <?php /** * WooCommerce Admin Personalize Your Store Note Provider. * * Adds a note to the merchant's inbox prompting them to personalize their store. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Personalize_Store */ class PersonalizeStore { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-personalize-store'; /** * Get the note. * * @return Note */ public static function get_note() { // Only show the note to stores with homepage. $homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false ); if ( ! $homepage_id ) { return; } // Show the note after task list is done. $is_task_list_complete = get_option( 'woocommerce_task_list_complete', false ); // We want to show the note after day 5. $five_days_in_seconds = 5 * DAY_IN_SECONDS; if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', $five_days_in_seconds ) && ! $is_task_list_complete ) { return; } $content = __( 'The homepage is one of the most important entry points in your store. When done right it can lead to higher conversions and engagement. Don\'t forget to personalize the homepage that we created for your store during the onboarding.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Personalize your store\'s homepage', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'personalize-homepage', __( 'Personalize homepage', 'woocommerce' ), admin_url( 'post.php?post=' . $homepage_id . '&action=edit' ), Note::E_WC_ADMIN_NOTE_ACTIONED ); return $note; } } Notes/AddFirstProduct.php 0000644 00000005435 15073235522 0011421 0 ustar 00 <?php /** * WooCommerce Admin: Add First Product. * * Adds a note (type `email`) to bring the client back to the store setup flow. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Add_First_Product. */ class AddFirstProduct { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-add-first-product-note'; /** * Get the note. * * @return Note */ public static function get_note() { if ( ! self::wc_admin_active_for( 2 * DAY_IN_SECONDS ) || self::wc_admin_active_for( 5 * DAY_IN_SECONDS ) ) { return; } // Don't show if there is a product. $query = new \WC_Product_Query( array( 'limit' => 1, 'return' => 'ids', 'status' => array( 'publish' ), ) ); $products = $query->get_products(); if ( 0 !== count( $products ) ) { return; } // Don't show if there is an orders. $args = array( 'limit' => 1, 'return' => 'ids', ); $orders = wc_get_orders( $args ); if ( 0 !== count( $orders ) ) { return; } // If you're updating the following please use sprintf to separate HTML tags. // https://github.com/woocommerce/woocommerce-admin/pull/6617#discussion_r596889685. $content_lines = array( '{greetings}<br/><br/>', /* translators: %s: line break */ sprintf( __( 'Nice one; you\'ve created a WooCommerce store! Now it\'s time to add your first product and get ready to start selling.%s', 'woocommerce' ), '<br/><br/>' ), __( 'There are three ways to add your products: you can <strong>create products manually, import them at once via CSV file</strong>, or <strong>migrate them from another service</strong>.<br/><br/>', 'woocommerce' ), /* translators: %1$s is an open anchor tag (<a>) and %2$s is a close link tag (</a>). */ sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '<a href="https://woo.com/document/managing-products/?utm_source=help_panel&utm_medium=product">', '</a>' ), ); $additional_data = array( 'role' => 'administrator', ); $note = new Note(); $note->set_title( __( 'Add your first product', 'woocommerce' ) ); $note->set_content( implode( '', $content_lines ) ); $note->set_content_data( (object) $additional_data ); $note->set_image( plugins_url( '/images/admin_notes/dashboard-widget-setup.png', WC_ADMIN_PLUGIN_FILE ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_EMAIL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'add-first-product', __( 'Add a product', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&task=products' ) ); return $note; } } Notes/EmailNotification.php 0000644 00000012250 15073235522 0011747 0 ustar 00 <?php /** * Handles emailing user notes. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; if ( ! defined( 'ABSPATH' ) ) { exit; } use Automattic\WooCommerce\Admin\Notes\Notes; /** * Include dependencies. */ if ( ! class_exists( 'WC_Email', false ) ) { include_once WC_ABSPATH . 'includes/emails/class-wc-email.php'; } /** * EmailNotification Class. */ class EmailNotification extends \WC_Email { /** * Constructor. * * @param Note $note The notification to send. */ public function __construct( $note ) { $this->note = $note; $this->id = 'merchant_notification'; $this->template_base = WC_ADMIN_ABSPATH . 'includes/react-admin/emails/'; $this->placeholders = array( '{greetings}' => __( 'Hi there,', 'woocommerce' ), ); // Call parent constructor. parent::__construct(); } /** * This email has no user-facing settings. */ public function init_form_fields() {} /** * This email has no user-facing settings. */ public function init_settings() {} /** * Return template filename. * * @param string $type Type of email to send. * @return string */ public function get_template_filename( $type = 'html' ) { if ( ! in_array( $type, array( 'html', 'plain' ), true ) ) { return; } $content_data = $this->note->get_content_data(); $template_filename = "{$type}-merchant-notification.php"; if ( isset( $content_data->{"template_{$type}"} ) && file_exists( $this->template_base . $content_data->{ "template_{$type}" } ) ) { $template_filename = $content_data[ "template_{$type}" ]; } return $template_filename; } /** * Return email type. * * @return string */ public function get_email_type() { return class_exists( 'DOMDocument' ) ? 'html' : 'plain'; } /** * Get email heading. * * @return string */ public function get_default_heading() { $content_data = $this->note->get_content_data(); if ( isset( $content_data->heading ) ) { return $content_data->heading; } return $this->note->get_title(); } /** * Get email headers. * * @return string */ public function get_headers() { $header = 'Content-Type: ' . $this->get_content_type() . "\r\n"; return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this ); } /** * Get email subject. * * @return string */ public function get_default_subject() { return $this->note->get_title(); } /** * Get note content. * * @return string */ public function get_note_content() { return $this->note->get_content(); } /** * Get note image. * * @return string */ public function get_image() { return $this->note->get_image(); } /** * Get email action. * * @return stdClass */ public function get_actions() { return $this->note->get_actions(); } /** * Get content html. * * @return string */ public function get_content_html() { return wc_get_template_html( $this->get_template_filename( 'html' ), array( 'email_actions' => $this->get_actions(), 'email_content' => $this->format_string( $this->get_note_content() ), 'email_heading' => $this->format_string( $this->get_heading() ), 'email_image' => $this->get_image(), 'sent_to_admin' => true, 'plain_text' => false, 'email' => $this, 'opened_tracking_url' => $this->opened_tracking_url, 'trigger_note_action_url' => $this->trigger_note_action_url, ), '', $this->template_base ); } /** * Get content plain. * * @return string */ public function get_content_plain() { return wc_get_template_html( $this->get_template_filename( 'plain' ), array( 'email_heading' => $this->format_string( $this->get_heading() ), 'email_content' => $this->format_string( $this->get_note_content() ), 'email_actions' => $this->get_actions(), 'sent_to_admin' => true, 'plain_text' => true, 'email' => $this, 'trigger_note_action_url' => $this->trigger_note_action_url, ), '', $this->template_base ); } /** * Trigger the sending of this email. * * @param string $user_email Email to send the note. * @param int $user_id User id to to track the note. * @param string $user_name User's name. */ public function trigger( $user_email, $user_id, $user_name ) { $this->recipient = $user_email; $this->opened_tracking_url = sprintf( '%1$s/wp-json/wc-analytics/admin/notes/tracker/%2$d/user/%3$d', site_url(), $this->note->get_id(), $user_id ); $this->trigger_note_action_url = sprintf( '%1$s&external_redirect=1¬e=%2$d&user=%3$d&action=', wc_admin_url(), $this->note->get_id(), $user_id ); if ( $user_name ) { /* translators: %s = merchant name */ $this->placeholders['{greetings}'] = sprintf( __( 'Hi %s,', 'woocommerce' ), $user_name ); } $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); Notes::record_tracks_event_with_user( $user_id, 'email_note_sent', array( 'note_name' => $this->note->get_name() ) ); } } Notes/OnboardingPayments.php 0000644 00000003354 15073235522 0012161 0 ustar 00 <?php /** * WooCommerce Admin: Payments reminder note. * * Adds a notes to complete the payment methods. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Onboarding_Payments. */ class OnboardingPayments { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-onboarding-payments-reminder'; /** * Get the note. * * @return Note */ public static function get_note() { // We want to show the note after five days. if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 5 * DAY_IN_SECONDS ) ) { return; } // Check to see if any gateways have been added. $gateways = WC()->payment_gateways->get_available_payment_gateways(); $enabled_gateways = array_filter( $gateways, function( $gateway ) { return 'yes' === $gateway->enabled; } ); if ( ! empty( $enabled_gateways ) ) { return; } $note = new Note(); $note->set_title( __( 'Start accepting payments on your store!', 'woocommerce' ) ); $note->set_content( __( 'Take payments with the provider that’s right for you - choose from 100+ payment gateways for WooCommerce.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'view-payment-gateways', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin: Customize your online store with WooCommerce blocks. * * Adds a note to customize the client online store with WooCommerce blocks. * * @package WooCommerce\Admin */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Customize_Store_With_Blocks. */ class CustomizeStoreWithBlocks { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-customize-store-with-blocks'; /** * Get the note. * * @return Note */ public static function get_note() { $onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() ); // Confirm that $onboarding_profile is set. if ( empty( $onboarding_profile ) ) { return; } // Make sure that the person who filled out the OBW was not setting up // the store for their customer/client. if ( ! isset( $onboarding_profile['setup_client'] ) || $onboarding_profile['setup_client'] ) { return; } // We want to show the note after fourteen days. if ( ! self::is_wc_admin_active_in_date_range( 'week-1-4', 14 * DAY_IN_SECONDS ) ) { return; } // Don't show if there aren't products. $query = new \WC_Product_Query( array( 'limit' => 1, 'return' => 'ids', 'status' => array( 'publish' ), ) ); $products = $query->get_products(); if ( 0 === count( $products ) ) { return; } $note = new Note(); $note->set_title( __( 'Customize your online store with WooCommerce blocks', 'woocommerce' ) ); $note->set_content( __( 'With our blocks, you can select and display products, categories, filters, and more virtually anywhere on your site — no need to use shortcodes or edit lines of code. Learn more about how to use each one of them.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'customize-store-with-blocks', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin Mobile App Note Provider. * * Adds a note to the merchant's inbox showing the benefits of the mobile app. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Mobile_App */ class MobileApp { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-mobile-app'; /** * Get the note. * * @return Note */ public static function get_note() { // We want to show the mobile app note after day 2. $two_days_in_seconds = 2 * DAY_IN_SECONDS; if ( ! self::is_wc_admin_active_in_date_range( 'week-1', $two_days_in_seconds ) ) { return; } $content = __( 'Install the WooCommerce mobile app to manage orders, receive sales notifications, and view key metrics — wherever you are.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Install Woo mobile app', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_medium=product' ); return $note; } } Notes/CouponPageMoved.php 0000644 00000007463 15073235522 0011416 0 ustar 00 <?php /** * WooCommerce Admin Coupon Page Moved provider. * * Adds a notice when the store manager access the coupons page via the old WooCommerce > Coupons menu. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait; use stdClass; use WC_Data_Store; /** * Coupon_Page_Moved class. */ class CouponPageMoved { use NoteTraits, CouponsMovedTrait; const NOTE_NAME = 'wc-admin-coupon-page-moved'; /** * Initialize our hooks. */ public function init() { if ( ! wc_coupons_enabled() ) { return; } add_action( 'admin_init', [ $this, 'possibly_add_note' ] ); add_action( 'admin_init', [ $this, 'redirect_to_coupons' ] ); add_action( 'woocommerce_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] ); } /** * Checks if a note can and should be added. * * @return bool */ public static function can_be_added() { if ( ! wc_coupons_enabled() ) { return false; } // Don't add the notice if the legacy coupon menu is already disabled. if ( ! self::should_display_legacy_menu() ) { return false; } // Don't add the notice if it's been hidden by the user before. if ( self::has_dismissed_note() ) { return false; } // If we already have a notice, don't add a new one. if ( self::has_unactioned_note() ) { return false; } return isset( $_GET[ self::$query_key ] ) && (bool) $_GET[ self::$query_key ]; // phpcs:ignore WordPress.Security.NonceVerification } /** * Get the note object for this class. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Coupon management has moved!', 'woocommerce' ) ); $note->set_content( __( 'Coupons can now be managed from Marketing > Coupons. Click the button below to remove the legacy WooCommerce > Coupons menu item.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( new stdClass() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'remove-legacy-coupon-menu', __( 'Remove legacy coupon menu', 'woocommerce' ), wc_admin_url( '&action=remove-coupon-menu' ), Note::E_WC_ADMIN_NOTE_ACTIONED ); return $note; } /** * Find notes that have not been actioned. * * @return bool */ protected static function has_unactioned_note() { $note = Notes::get_note_by_name( self::NOTE_NAME ); if ( ! $note ) { return false; } return $note->get_status() === 'unactioned'; } /** * Whether any notes have been dismissed by the user previously. * * @return bool */ protected static function has_dismissed_note() { $note = Notes::get_note_by_name( self::NOTE_NAME ); if ( ! $note ) { return false; } return ! $note->get_is_deleted(); } /** * Get the data store object. * * @return DataStore The data store object. */ protected static function get_data_store() { return WC_Data_Store::load( 'admin-note' ); } /** * Safe redirect to the coupon page to force page refresh. */ public function redirect_to_coupons() { /* phpcs:disable WordPress.Security.NonceVerification */ if ( ! isset( $_GET['page'] ) || 'wc-admin' !== $_GET['page'] || ! isset( $_GET['action'] ) || 'remove-coupon-menu' !== $_GET['action'] || ! defined( 'WC_ADMIN_PLUGIN_FILE' ) ) { return; } /* phpcs:enable */ $this->display_legacy_menu( false ); wp_safe_redirect( self::get_management_url( 'coupons' ) ); exit; } /** * Disable legacy coupon menu when installing for the first time. */ public function disable_legacy_menu_for_new_install() { $this->display_legacy_menu( false ); } } Notes/ManageOrdersOnTheGo.php 0000644 00000003041 15073235522 0012142 0 ustar 00 <?php /** * WooCommerce Admin Manage orders on the go note. * * Adds a note to download the mobile app to manage orders on the go. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Manage_Orders_On_The_Go */ class ManageOrdersOnTheGo { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-manage-orders-on-the-go'; /** * Get the note. * * @return Note|null */ public static function get_note() { // Only add this note if this store is at least 6 months old. if ( ! self::is_wc_admin_active_in_date_range( 'month-6+' ) ) { return; } // Check that the previous mobile app notes have not been actioned. if ( MobileApp::has_note_been_actioned() ) { return; } if ( RealTimeOrderAlerts::has_note_been_actioned() ) { return; } $note = new Note(); $note->set_title( __( 'Manage your orders on the go', 'woocommerce' ) ); $note->set_content( __( 'Look for orders, customer info, and process refunds in one click with the Woo app.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_source=inbox&utm_medium=product' ); return $note; } } Notes/ChoosingTheme.php 0000644 00000002676 15073235522 0011120 0 ustar 00 <?php /** * WooCommerce Admin (Dashboard) choosing a theme note * * Adds notes to the merchant's inbox about choosing a theme. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Giving_Feedback_Notes */ class ChoosingTheme { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-choosing-a-theme'; /** * Get the note. * * @return Note */ public static function get_note() { // We need to show choosing a theme notification after 1 day of install. if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS ) ) { return; } // Otherwise, create our new note. $note = new Note(); $note->set_title( __( 'Choosing a theme?', 'woocommerce' ) ); $note->set_content( __( 'Check out the themes that are compatible with WooCommerce and choose one aligned with your brand and business needs.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'visit-the-theme-marketplace', __( 'Visit the theme marketplace', 'woocommerce' ), 'https://woo.com/product-category/themes/?utm_source=inbox&utm_medium=product' ); return $note; } } Notes/OnlineClothingStore.php 0000644 00000005261 15073235522 0012306 0 ustar 00 <?php /** * WooCommerce Admin: Start your online clothing store. * * Adds a note to ask the client if they are considering starting an online * clothing store. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Online_Clothing_Store. */ class OnlineClothingStore { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-online-clothing-store'; /** * Returns whether the industries includes fashion-apparel-accessories. * * @param array $industries The industries to search. * * @return bool Whether the industries includes fashion-apparel-accessories. */ private static function is_in_fashion_industry( $industries ) { foreach ( $industries as $industry ) { if ( 'fashion-apparel-accessories' === $industry['slug'] ) { return true; } } return false; } /** * Get the note. * * @return Note */ public static function get_note() { // We want to show the note after two days. if ( ! self::is_wc_admin_active_in_date_range( 'week-1', 2 * DAY_IN_SECONDS ) ) { return; } $onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() ); // Confirm that $onboarding_profile is set. if ( empty( $onboarding_profile ) ) { return; } // Make sure that the person who filled out the OBW was not setting up // the store for their customer/client. if ( ! isset( $onboarding_profile['setup_client'] ) || $onboarding_profile['setup_client'] ) { return; } // We need to show the notification when the industry is // fashion/apparel/accessories. if ( ! isset( $onboarding_profile['industry'] ) ) { return; } if ( ! self::is_in_fashion_industry( $onboarding_profile['industry'] ) ) { return; } $note = new Note(); $note->set_title( __( 'Start your online clothing store', 'woocommerce' ) ); $note->set_content( __( 'Starting a fashion website is exciting but it may seem overwhelming as well. In this article, we\'ll walk you through the setup process, teach you to create successful product listings, and show you how to market to your ideal audience.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'online-clothing-store', __( 'Learn more', 'woocommerce' ), 'https://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 <?php /** * WooCommerce Admin note on how to migrate from Magento. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Features\Onboarding; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * MagentoMigration */ class MagentoMigration { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-magento-migration'; /** * Attach hooks. */ public function __construct() { add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( __CLASS__, 'possibly_add_note' ) ); add_action( 'woocommerce_admin_magento_migration_note', array( __CLASS__, 'save_note' ) ); } /** * Add the note if it passes predefined conditions. */ public static function possibly_add_note() { $onboarding_profile = get_option( OnboardingProfile::DATA_OPTION, array() ); if ( empty( $onboarding_profile ) ) { return; } if ( ! isset( $onboarding_profile['other_platform'] ) || 'magento' !== $onboarding_profile['other_platform'] ) { return; } if ( ! isset( $onboarding_profile['setup_client'] ) || $onboarding_profile['setup_client'] ) { return; } WC()->queue()->schedule_single( time() + ( 5 * MINUTE_IN_SECONDS ), 'woocommerce_admin_magento_migration_note' ); } /** * Save the note to the database. */ public static function save_note() { $note = self::get_note(); if ( self::note_exists() ) { return; } $note->save(); } /** * Get the note. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'How to Migrate from Magento to WooCommerce', 'woocommerce' ) ); $note->set_content( __( 'Changing platforms might seem like a big hurdle to overcome, but it is easier than you might think to move your products, customers, and orders to WooCommerce. This article will help you with going through this process.', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox' ); return $note; } } Notes/RealTimeOrderAlerts.php 0000644 00000003002 15073235522 0012215 0 ustar 00 <?php /** * WooCommerce Admin Real Time Order Alerts Note. * * Adds a note to download the mobile app to monitor store activity. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Real_Time_Order_Alerts */ class RealTimeOrderAlerts { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-real-time-order-alerts'; /** * Get the note. * * @return Note */ public static function get_note() { // Only add this note if the store is 3 months old. if ( ! self::is_wc_admin_active_in_date_range( 'month-3-6' ) ) { return; } // Check that the previous mobile app note was not actioned. if ( MobileApp::has_note_been_actioned() ) { return; } $content = __( 'Get notifications about store activity, including new orders and product reviews directly on your mobile devices with the Woo app.', 'woocommerce' ); $note = new Note(); $note->set_title( __( 'Get real-time order alerts anywhere', 'woocommerce' ) ); $note->set_content( $content ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_source=inbox&utm_medium=product' ); return $note; } } Notes/EditProductsOnTheMove.php 0000644 00000003254 15073235522 0012553 0 ustar 00 <?php /** * WooCommerce Admin Edit products on the move note. * * Adds a note to download the mobile app. */ namespace Automattic\WooCommerce\Internal\Admin\Notes; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; /** * Edit_Products_On_The_Move */ class EditProductsOnTheMove { /** * Note traits. */ use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-admin-edit-products-on-the-move'; /** * Get the note. * * @return Note */ public static function get_note() { // Only add this note if this store is at least a year old. $year_in_seconds = 365 * DAY_IN_SECONDS; if ( ! self::wc_admin_active_for( $year_in_seconds ) ) { return; } // Check that the previous mobile app notes have not been actioned. if ( MobileApp::has_note_been_actioned() ) { return; } if ( RealTimeOrderAlerts::has_note_been_actioned() ) { return; } if ( ManageOrdersOnTheGo::has_note_been_actioned() ) { return; } if ( PerformanceOnMobile::has_note_been_actioned() ) { return; } $note = new Note(); $note->set_title( __( 'Edit products on the move', 'woocommerce' ) ); $note->set_content( __( 'Edit and create new products from your mobile devices with the Woo app', 'woocommerce' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_source=inbox&utm_medium=product' ); return $note; } } Schedulers/MailchimpScheduler.php 0000644 00000007160 15073235522 0013130 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Schedulers; /** * Class MailchimpScheduler * * @package Automattic\WooCommerce\Admin\Schedulers */ class MailchimpScheduler { const SUBSCRIBE_ENDPOINT = 'https://woocommerce.com/wp-json/wccom/v1/subscribe'; const SUBSCRIBE_ENDPOINT_DEV = 'http://woocommerce.test/wp-json/wccom/v1/subscribe'; const SUBSCRIBED_OPTION_NAME = 'woocommerce_onboarding_subscribed_to_mailchimp'; const SUBSCRIBED_ERROR_COUNT_OPTION_NAME = 'woocommerce_onboarding_subscribed_to_mailchimp_error_count'; const MAX_ERROR_THRESHOLD = 3; const LOGGER_CONTEXT = 'mailchimp_scheduler'; /** * The logger instance. * * @var \WC_Logger_Interface|null */ private $logger; /** * MailchimpScheduler constructor. * * @internal * @param \WC_Logger_Interface|null $logger Logger instance. */ public function __construct( \WC_Logger_Interface $logger = null ) { if ( null === $logger ) { $logger = wc_get_logger(); } $this->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 <?php /** * Order syncing related functions and actions. */ namespace Automattic\WooCommerce\Internal\Admin\Schedulers; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use Automattic\WooCommerce\Admin\Overrides\Order; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\Utilities\OrderUtil; /** * OrdersScheduler Class. */ class OrdersScheduler extends ImportScheduler { /** * Slug to identify the scheduler. * * @var string */ public static $name = 'orders'; /** * Attach order lookup update hooks. * * @internal */ public static function init() { // Activate WC_Order extension. \Automattic\WooCommerce\Admin\Overrides\Order::add_filters(); \Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters(); // Order and refund data must be run on these hooks to ensure meta data is set. add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) ); add_filter( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) ); add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) ); OrdersStatsDataStore::init(); CouponsDataStore::init(); ProductsDataStore::init(); TaxesDataStore::init(); parent::init(); } /** * Add customer dependencies. * * @internal * @return array */ public static function get_dependencies() { return array( 'import_batch_init' => \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 <?php /** * Customer syncing related functions and actions. */ namespace Automattic\WooCommerce\Internal\Admin\Schedulers; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; /** * CustomersScheduler Class. */ class CustomersScheduler extends ImportScheduler { /** * Slug to identify the scheduler. * * @var string */ public static $name = 'customers'; /** * Attach customer lookup update hooks. * * @internal */ public static function init() { CustomersDataStore::init(); parent::init(); } /** * Add customer dependencies. * * @internal * @return array */ public static function get_dependencies() { return array( 'delete_batch_init' => OrdersScheduler::get_action( 'delete_batch_init' ), ); } /** * Get the customer IDs and total count that need to be synced. * * @internal * @param int $limit Number of records to retrieve. * @param int $page Page number. * @param int|bool $days Number of days prior to current date to limit search results. * @param bool $skip_existing Skip already imported customers. */ public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) { $customer_roles = apply_filters( 'woocommerce_analytics_import_customer_roles', array( 'customer' ) ); $query_args = array( 'fields' => 'ID', 'orderby' => 'ID', 'order' => 'ASC', 'number' => $limit, 'paged' => $page, 'role__in' => $customer_roles, ); if ( is_int( $days ) ) { $query_args['date_query'] = array( 'after' => gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ), ); } if ( $skip_existing ) { add_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) ); } $customer_query = new \WP_User_Query( $query_args ); remove_action( 'pre_user_query', array( __CLASS__, 'exclude_existing_customers_from_query' ) ); return (object) array( 'total' => $customer_query->get_total(), 'ids' => $customer_query->get_results(), ); } /** * Exclude users that already exist in our customer lookup table. * * Meant to be hooked into 'pre_user_query' action. * * @internal * @param WP_User_Query $wp_user_query WP_User_Query to modify. */ public static function exclude_existing_customers_from_query( $wp_user_query ) { global $wpdb; $wp_user_query->query_where .= " AND NOT EXISTS ( SELECT ID FROM {$wpdb->prefix}wc_customer_lookup WHERE {$wpdb->prefix}wc_customer_lookup.user_id = {$wpdb->users}.ID )"; } /** * Get total number of rows imported. * * @internal * @return int */ public static function get_total_imported() { global $wpdb; return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" ); } /** * Imports a single customer. * * @internal * @param int $user_id User ID. * @return void */ public static function import( $user_id ) { CustomersDataStore::update_registered_customer( $user_id ); } /** * Delete a batch of customers. * * @internal * @param int $batch_size Number of items to delete. * @return void */ public static function delete( $batch_size ) { global $wpdb; $customer_ids = $wpdb->get_col( $wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_customer_lookup ORDER BY customer_id ASC LIMIT %d", $batch_size ) ); foreach ( $customer_ids as $customer_id ) { CustomersDataStore::delete_customer( $customer_id ); } } } Schedulers/ImportScheduler.php 0000644 00000011457 15073235522 0012503 0 ustar 00 <?php /** * Import related functions and actions. */ namespace Automattic\WooCommerce\Internal\Admin\Schedulers; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use Automattic\WooCommerce\Admin\Schedulers\SchedulerTraits; /** * ImportScheduler class. */ abstract class ImportScheduler implements ImportInterface { /** * Import stats option name. */ const IMPORT_STATS_OPTION = 'woocommerce_admin_import_stats'; /** * Scheduler traits. */ use SchedulerTraits { get_batch_sizes as get_scheduler_batch_sizes; } /** * Returns true if an import is in progress. * * @internal * @return bool */ public static function is_importing() { $pending_jobs = self::queue()->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 <?php /** * Import related abstract functions. */ namespace Automattic\WooCommerce\Internal\Admin\Schedulers; interface ImportInterface { /** * Get items based on query and return IDs along with total available. * * @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 items. */ public static function get_items( $limit, $page, $days, $skip_existing ); /** * Get total number of items already imported. * * @internal * @return null */ public static function get_total_imported(); } RemoteFreeExtensions/DefaultFreeExtensions.php 0000644 00000074732 15073235522 0015661 0 ustar 00 <?php /** * Gets a list of fallback methods if remote fetching is disabled. */ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions; use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways; defined( 'ABSPATH' ) || exit; /** * Default Free Extensions */ class DefaultFreeExtensions { /** * Get default specs. * * @return array Default specs. */ public static function get_all() { $bundles = array( array( 'key' => 'obw/basics', 'title' => __( 'Get the basics', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'woocommerce-payments' ), self::get_plugin( 'woocommerce-services:shipping' ), self::get_plugin( 'woocommerce-services:tax' ), self::get_plugin( 'jetpack' ), ), ), array( 'key' => 'obw/grow', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'mailpoet' ), self::get_plugin( 'google-listings-and-ads' ), self::get_plugin( 'pinterest-for-woocommerce' ), self::get_plugin( 'facebook-for-woocommerce' ), ), ), array( 'key' => 'task-list/reach', 'title' => __( 'Reach out to customers', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'mailpoet:alt' ), self::get_plugin( 'mailchimp-for-woocommerce' ), self::get_plugin( 'klaviyo' ), ), ), array( 'key' => 'task-list/grow', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'google-listings-and-ads:alt' ), self::get_plugin( 'tiktok-for-business' ), self::get_plugin( 'pinterest-for-woocommerce:alt' ), self::get_plugin( 'facebook-for-woocommerce:alt' ), ), ), array( 'key' => 'obw/core-profiler', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => self::with_core_profiler_fields( array( self::get_plugin( 'woocommerce-payments' ), self::get_plugin( 'woocommerce-services:shipping' ), self::get_plugin( 'jetpack' ), self::get_plugin( 'pinterest-for-woocommerce' ), self::get_plugin( 'mailpoet' ), self::get_plugin( 'google-listings-and-ads' ), self::get_plugin( 'woocommerce-services:tax' ), self::get_plugin( 'tiktok-for-business' ), ) ), ), ); $bundles = wp_json_encode( $bundles ); return json_decode( $bundles ); } /** * Get the plugin arguments by slug. * * @param string $slug Slug. * @return array */ public static function get_plugin( $slug ) { $plugins = array( 'google-listings-and-ads' => array( 'min_php_version' => '7.4', 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ), '<a href="https://woo.com/products/google-listings-and-ads" target="_blank">', '</a>' ), 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'google-listings-and-ads' ), ), ), ), ), ), 'google-listings-and-ads:alt' => array( 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, ), 'facebook-for-woocommerce' => array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'description' => __( 'List products and create ads on Facebook and Instagram with <a href="https://woo.com/products/facebook/">Facebook for WooCommerce</a>', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-facebook', 'is_visible' => false, 'is_built_by_wc' => false, ), 'facebook-for-woocommerce:alt' => array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'description' => __( 'List products and create ads on Facebook and Instagram.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-facebook', 'is_visible' => false, 'is_built_by_wc' => false, ), 'pinterest-for-woocommerce' => array( 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinners searching for ideas and things to buy.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => true, 'min_php_version' => '7.3', ), 'pinterest-for-woocommerce:alt' => array( 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => true, ), 'mailpoet' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ), 'mailchimp-for-woocommerce' => array( 'name' => __( 'Mailchimp', 'woocommerce' ), 'description' => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailchimp-woocommerce', 'is_built_by_wc' => false, ), 'klaviyo' => array( 'name' => __( 'Klaviyo', 'woocommerce' ), 'description' => __( 'Grow and retain customers with intelligent, impactful email and SMS marketing automation and a consolidated view of customer interactions.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/klaviyo.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=klaviyo_settings', 'is_built_by_wc' => false, ), 'woocommerce-payments' => array( 'name' => __( 'WooPayments', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/wcpay.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Accept credit cards and other popular payment methods with %1$sWooPayments%2$s', 'woocommerce' ), '<a href="https://woo.com/products/woocommerce-payments" target="_blank">', '</a>' ), 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ES', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CY', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'EE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LV', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'JP', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AE', 'operation' => '=', ), ), ), DefaultPaymentGateways::get_rules_for_cbd( false ), ), 'is_built_by_wc' => true, 'min_wp_version' => '5.9', ), 'woocommerce-services:shipping' => array( 'name' => __( 'WooCommerce Shipping', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Print shipping labels with %1$sWooCommerce Shipping%2$s', 'woocommerce' ), '<a href="https://woo.com/products/shipping" target="_blank">', '</a>' ), 'is_visible' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'woocommerce-services' ), ), ), ), array( 'type' => 'or', 'operands' => array( array( array( 'type' => 'option', 'transformers' => array( array( 'use' => 'dot_notation', 'arguments' => array( 'path' => 'product_types', ), ), array( 'use' => 'count', ), ), 'option_name' => 'woocommerce_onboarding_profile', 'value' => 1, 'default' => array(), 'operation' => '!=', ), ), array( array( 'type' => 'option', 'transformers' => array( array( 'use' => 'dot_notation', 'arguments' => array( 'path' => 'product_types.0', ), ), ), 'option_name' => 'woocommerce_onboarding_profile', 'value' => 'downloads', 'default' => '', 'operation' => '!=', ), ), ), ), ), 'is_built_by_wc' => true, ), 'woocommerce-services:tax' => array( 'name' => __( 'WooCommerce Tax', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Get automated sales tax with %1$sWooCommerce Tax%2$s', 'woocommerce' ), '<a href="https://woo.com/products/tax" target="_blank">', '</a>' ), 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), ), ), array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'woocommerce-services' ), ), ), ), ), 'is_built_by_wc' => true, ), 'jetpack' => array( 'name' => __( 'Jetpack', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/jetpack.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Enhance speed and security with %1$sJetpack%2$s', 'woocommerce' ), '<a href="https://woo.com/products/jetpack" target="_blank">', '</a>' ), 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'jetpack' ), ), ), ), ), 'is_built_by_wc' => false, 'min_wp_version' => '6.0', ), 'mailpoet' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Level up your email marketing with %1$sMailPoet%2$s', 'woocommerce' ), '<a href="https://woo.com/products/mailpoet" target="_blank">', '</a>' ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'mailpoet' ), ), ), ), ), 'is_built_by_wc' => true, ), 'mailpoet:alt' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ), 'tiktok-for-business' => array( 'name' => __( 'TikTok for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ), 'description' => __( 'Grow your online sales by promoting your products on TikTok to over one billion monthly active users around the world.', 'woocommerce' ), 'manage_url' => 'admin.php?page=tiktok', 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MX', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ES', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MY', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ID', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'VN', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'TH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'KR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'UA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'TR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'JP', 'operation' => '=', ), ), ), ), 'is_built_by_wc' => false, ), 'tiktok-for-business:alt' => array( 'name' => __( 'TikTok for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Create ad campaigns and reach one billion global users with %1$sTikTok for WooCommerce%2$s', 'woocommerce' ), '<a href="https://woo.com/products/tiktok-for-woocommerce" target="_blank">', '</a>' ), 'manage_url' => 'admin.php?page=tiktok', 'is_built_by_wc' => false, 'is_visible' => false, ), ); $plugin = $plugins[ $slug ]; $plugin['key'] = $slug; return $plugin; } /** * Decorate plugin data with core profiler fields. * * - Updated description for the core-profiler. * - Adds learn_more_link and label. * - Adds install_priority, which is used to sort the plugins. The value is determined by the plugin size. Lower = smaller. * * @param array $plugins Array of plugins. * * @return array */ public static function with_core_profiler_fields( array $plugins ) { $_plugins = array( 'woocommerce-payments' => array( 'label' => __( 'Get paid with WooPayments', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ), 'learn_more_link' => 'https://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 <?php /** * Handles running payment method specs */ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\DefaultFreeExtensions; /** * Remote Payment Methods engine. * This goes through the specs and gets eligible payment methods. */ class Init { /** * Constructor. */ public function __construct() { add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) ); } /** * Go through the specs and run them. * * @param array $allowed_bundles Optional array of allowed bundles to be returned. * @return array */ public static function get_extensions( $allowed_bundles = array() ) { $bundles = array(); $specs = self::get_specs(); foreach ( $specs as $spec ) { $spec = (object) $spec; $bundle = (array) $spec; $bundle['plugins'] = array(); if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) { continue; } 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 <?php /** * Evaluates the spec and returns a status. */ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\PluginsHelper; use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator; /** * Evaluates the extension and returns it. */ class EvaluateExtension { /** * Evaluates the extension and returns it. * * @param object $extension The extension to evaluate. * @return object The evaluated extension. */ public static function evaluate( $extension ) { global $wp_version; $rule_evaluator = new RuleEvaluator(); if ( isset( $extension->is_visible ) ) { $is_visible = $rule_evaluator->evaluate( $extension->is_visible ); $extension->is_visible = $is_visible; } else { $extension->is_visible = true; } // Run PHP and WP version chcecks. if ( true === $extension->is_visible ) { if ( isset( $extension->min_php_version ) && ! version_compare( PHP_VERSION, $extension->min_php_version, '>=' ) ) { $extension->is_visible = false; } if ( isset( $extension->min_wp_version ) && ! version_compare( $wp_version, $extension->min_wp_version, '>=' ) ) { $extension->is_visible = false; } } $installed_plugins = PluginsHelper::get_installed_plugin_slugs(); $activated_plugins = PluginsHelper::get_active_plugin_slugs(); $extension->is_installed = in_array( explode( ':', $extension->key )[0], $installed_plugins, true ); $extension->is_activated = in_array( explode( ':', $extension->key )[0], $activated_plugins, true ); return $extension; } } RemoteFreeExtensions/RemoteFreeExtensionsDataSourcePoller.php 0000644 00000001371 15073235522 0020646 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions; /** * Specs data source poller class for remote free extensions. */ class RemoteFreeExtensionsDataSourcePoller extends \Automattic\WooCommerce\Admin\DataSourcePoller { const ID = 'remote_free_extensions'; const DATA_SOURCES = array( 'https://woocommerce.com/wp-json/wccom/obw-free-extensions/3.0/extensions.json', ); /** * Class instance. * * @var Analytics instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self( self::ID, self::DATA_SOURCES, array( 'spec_key' => 'key', ) ); } return self::$instance; } } ProductForm/ComponentTrait.php 0000644 00000001315 15073235522 0012473 0 ustar 00 <?php /** * Product Form Traits */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; defined( 'ABSPATH' ) || exit; /** * ComponentTrait class. */ trait ComponentTrait { /** * Component ID. * * @var string */ protected $id; /** * Plugin ID. * * @var string */ protected $plugin_id; /** * Product form component location. * * @var string */ protected $location; /** * Product form component order. * * @var number */ protected $order; /** * Return id. * * @return string */ public function get_id() { return $this->id; } /** * Return plugin id. * * @return string */ public function get_plugin_id() { return $this->plugin_id; } } ProductForm/FormFactory.php 0000644 00000016476 15073235522 0011776 0 ustar 00 <?php /** * WooCommerce Product Form Factory * * @package Woocommerce ProductForm */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; use WP_Error; /** * Factory that contains logic for the WooCommerce Product Form. */ class FormFactory { /** * Class instance. * * @var Form instance */ protected static $instance = null; /** * Store form fields. * * @var array */ protected static $form_fields = array(); /** * Store form cards. * * @var array */ protected static $form_subsections = array(); /** * Store form sections. * * @var array */ protected static $form_sections = array(); /** * Store form tabs. * * @var array */ protected static $form_tabs = array(); /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init. */ public function init() { } /** * Adds a field to the product form. * * @param string $id Field id. * @param string $plugin_id Plugin id. * @param array $args Array containing the necessary arguments. * $args = array( * 'type' => (string) Field type. Required. * 'section' => (string) Field location. Required. * 'order' => (int) Field order. * 'properties' => (array) Field properties. * 'name' => (string) Field name. * ). * @return Field|WP_Error New field or WP_Error. */ public static function add_field( $id, $plugin_id, $args ) { $new_field = self::create_item( 'field', 'Field', $id, $plugin_id, $args ); if ( is_wp_error( $new_field ) ) { return $new_field; } self::$form_fields[ $id ] = $new_field; return $new_field; } /** * Adds a Subsection to the product form. * * @param string $id Subsection id. * @param string $plugin_id Plugin id. * @param array $args Array containing the necessary arguments. * @return Subsection|WP_Error New subsection or WP_Error. */ public static function add_subsection( $id, $plugin_id, $args = array() ) { $new_subsection = self::create_item( 'subsection', 'Subsection', $id, $plugin_id, $args ); if ( is_wp_error( $new_subsection ) ) { return $new_subsection; } self::$form_subsections[ $id ] = $new_subsection; return $new_subsection; } /** * Adds a section to the product form. * * @param string $id Card id. * @param string $plugin_id Plugin id. * @param array $args Array containing the necessary arguments. * @return Section|WP_Error New section or WP_Error. */ public static function add_section( $id, $plugin_id, $args ) { $new_section = self::create_item( 'section', 'Section', $id, $plugin_id, $args ); if ( is_wp_error( $new_section ) ) { return $new_section; } self::$form_sections[ $id ] = $new_section; return $new_section; } /** * Adds a tab to the product form. * * @param string $id Card id. * @param string $plugin_id Plugin id. * @param array $args Array containing the necessary arguments. * @return Tab|WP_Error New section or WP_Error. */ public static function add_tab( $id, $plugin_id, $args ) { $new_tab = self::create_item( 'tab', 'Tab', $id, $plugin_id, $args ); if ( is_wp_error( $new_tab ) ) { return $new_tab; } self::$form_tabs[ $id ] = $new_tab; return $new_tab; } /** * Returns list of registered fields. * * @param array $sort_by key and order to sort by. * @return array list of registered fields. */ public static function get_fields( $sort_by = array( 'key' => 'order', 'order' => 'asc', ) ) { return self::get_items( 'field', 'Field', $sort_by ); } /** * Returns list of registered cards. * * @param array $sort_by key and order to sort by. * @return array list of registered cards. */ public static function get_subsections( $sort_by = array( 'key' => 'order', 'order' => 'asc', ) ) { return self::get_items( 'subsection', 'Subsection', $sort_by ); } /** * Returns list of registered sections. * * @param array $sort_by key and order to sort by. * @return array list of registered sections. */ public static function get_sections( $sort_by = array( 'key' => 'order', 'order' => 'asc', ) ) { return self::get_items( 'section', 'Section', $sort_by ); } /** * Returns list of registered tabs. * * @param array $sort_by key and order to sort by. * @return array list of registered tabs. */ public static function get_tabs( $sort_by = array( 'key' => 'order', 'order' => 'asc', ) ) { return self::get_items( 'tab', 'Tab', $sort_by ); } /** * Returns list of registered items. * * @param string $type Form component type. * @return array List of registered items. */ private static function get_item_list( $type ) { $mapping = array( 'field' => self::$form_fields, 'subsection' => self::$form_subsections, 'section' => self::$form_sections, 'tab' => self::$form_tabs, ); if ( array_key_exists( $type, $mapping ) ) { return $mapping[ $type ]; } return array(); } /** * Returns list of registered items. * * @param string $type Form component type. * @param class-string $class_name Class of component type. * @param array $sort_by key and order to sort by. * @return array list of registered items. */ private static function get_items( $type, $class_name, $sort_by = array( 'key' => 'order', 'order' => 'asc', ) ) { $item_list = self::get_item_list( $type ); $class = 'Automattic\\WooCommerce\\Internal\\Admin\\ProductForm\\' . $class_name; $items = array_values( $item_list ); if ( class_exists( $class ) && method_exists( $class, 'sort' ) ) { usort( $items, function ( $a, $b ) use ( $sort_by, $class ) { return $class::sort( $a, $b, $sort_by ); } ); } return $items; } /** * Creates a new item. * * @param string $type Form component type. * @param class-string $class_name Class of component type. * @param string $id Item id. * @param string $plugin_id Plugin id. * @param array $args additional arguments for item. * @return Field|Card|Section|Tab|WP_Error New product form item or WP_Error. */ private static function create_item( $type, $class_name, $id, $plugin_id, $args ) { $item_list = self::get_item_list( $type ); $class = 'Automattic\\WooCommerce\\Internal\\Admin\\ProductForm\\' . $class_name; if ( ! class_exists( $class ) ) { return new WP_Error( 'wc_product_form_' . $type . '_missing_form_class', sprintf( /* translators: 1: missing class name. */ esc_html__( '%1$s class does not exist.', 'woocommerce' ), $class ) ); } if ( isset( $item_list[ $id ] ) ) { return new WP_Error( 'wc_product_form_' . $type . '_duplicate_field_id', sprintf( /* translators: 1: Item type 2: Duplicate registered item id. */ esc_html__( 'You have attempted to register a duplicate form %1$s with WooCommerce Form: %2$s', 'woocommerce' ), $type, '`' . $id . '`' ) ); } $defaults = array( 'order' => 20, ); $item_arguments = wp_parse_args( $args, $defaults ); try { return new $class( $id, $plugin_id, $item_arguments ); } catch ( \Exception $e ) { return new WP_Error( 'wc_product_form_' . $type . '_class_creation', $e->getMessage() ); } } } ProductForm/Section.php 0000644 00000002234 15073235522 0011132 0 ustar 00 <?php /** * Handles product form section related methods. */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; /** * Section class. */ class Section extends Component { /** * Constructor * * @param string $id Section id. * @param string $plugin_id Plugin id. * @param array $additional_args Array containing additional arguments. * $args = array( * 'order' => (int) Section order. * 'title' => (string) Section description. * 'description' => (string) Section description. * ). * @throws \Exception If there are missing arguments. */ public function __construct( $id, $plugin_id, $additional_args ) { parent::__construct( $id, $plugin_id, $additional_args ); $this->required_arguments = array( 'title', ); $missing_arguments = self::get_missing_arguments( $additional_args ); if ( count( $missing_arguments ) > 0 ) { throw new \Exception( sprintf( /* translators: 1: Missing arguments list. */ esc_html__( 'You are missing required arguments of WooCommerce ProductForm Section: %1$s', 'woocommerce' ), join( ', ', $missing_arguments ) ) ); } } } ProductForm/Subsection.php 0000644 00000000304 15073235522 0011640 0 ustar 00 <?php /** * Handles product form SubSection related methods. */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; /** * SubSection class. */ class Subsection extends Component {} ProductForm/Field.php 0000644 00000002422 15073235522 0010550 0 ustar 00 <?php /** * Handles product form field related methods. */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; /** * Field class. */ class Field extends Component { /** * Constructor * * @param string $id Field id. * @param string $plugin_id Plugin id. * @param array $additional_args Array containing the necessary arguments. * $args = array( * 'type' => (string) Field type. Required. * 'section' => (string) Field location. Required. * 'order' => (int) Field order. * 'properties' => (array) Field properties. * ). * @throws \Exception If there are missing arguments. */ public function __construct( $id, $plugin_id, $additional_args ) { parent::__construct( $id, $plugin_id, $additional_args ); $this->required_arguments = array( 'type', 'section', 'properties.name', 'properties.label', ); $missing_arguments = self::get_missing_arguments( $additional_args ); if ( count( $missing_arguments ) > 0 ) { throw new \Exception( sprintf( /* translators: 1: Missing arguments list. */ esc_html__( 'You are missing required arguments of WooCommerce ProductForm Field: %1$s', 'woocommerce' ), join( ', ', $missing_arguments ) ) ); } } } ProductForm/Tab.php 0000644 00000002322 15073235522 0010232 0 ustar 00 <?php /** * Handles product form tab related methods. */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; /** * Field class. */ class Tab extends Component { /** * Constructor * * @param string $id Field id. * @param string $plugin_id Plugin id. * @param array $additional_args Array containing the necessary arguments. * $args = array( * 'name' => (string) Tab name. Required. * 'title' => (string) Tab title. Required. * 'order' => (int) Tab order. * 'properties' => (array) Tab properties. * ). * @throws \Exception If there are missing arguments. */ public function __construct( $id, $plugin_id, $additional_args ) { parent::__construct( $id, $plugin_id, $additional_args ); $this->required_arguments = array( 'name', 'title', ); $missing_arguments = self::get_missing_arguments( $additional_args ); if ( count( $missing_arguments ) > 0 ) { throw new \Exception( sprintf( /* translators: 1: Missing arguments list. */ esc_html__( 'You are missing required arguments of WooCommerce ProductForm Tab: %1$s', 'woocommerce' ), join( ', ', $missing_arguments ) ) ); } } } ProductForm/Component.php 0000644 00000005547 15073235522 0011502 0 ustar 00 <?php /** * Abstract class for product form components. */ namespace Automattic\WooCommerce\Internal\Admin\ProductForm; /** * Component class. */ abstract class Component { /** * Product Component traits. */ use ComponentTrait; /** * Component additional arguments. * * @var array */ protected $additional_args; /** * Constructor * * @param string $id Component id. * @param string $plugin_id Plugin id. * @param array $additional_args Array containing additional arguments. */ public function __construct( $id, $plugin_id, $additional_args ) { $this->id = $id; $this->plugin_id = $plugin_id; $this->additional_args = $additional_args; } /** * Component arguments. * * @return array */ public function get_additional_args() { return $this->additional_args; } /** * Component arguments. * * @param string $key key of argument. * @return mixed */ public function get_additional_argument( $key ) { return self::get_argument_from_path( $this->additional_args, $key ); } /** * Get the component as JSON. * * @return array */ public function get_json() { return array_merge( array( 'id' => $this->get_id(), 'plugin_id' => $this->get_plugin_id(), ), $this->get_additional_args() ); } /** * Sorting function for product form component. * * @param Component $a Component a. * @param Component $b Component b. * @param array $sort_by key and order to sort by. * @return int */ public static function sort( $a, $b, $sort_by = array() ) { $key = $sort_by['key']; $a_val = $a->get_additional_argument( $key ); $b_val = $b->get_additional_argument( $key ); if ( 'asc' === $sort_by['order'] ) { return $a_val <=> $b_val; } else { return $b_val <=> $a_val; } } /** * Gets argument by dot notation path. * * @param array $arguments Arguments array. * @param string $path Path for argument key. * @param string $delimiter Path delimiter, default: '.'. * @return mixed|null */ public static function get_argument_from_path( $arguments, $path, $delimiter = '.' ) { $path_keys = explode( $delimiter, $path ); $num_keys = 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 <?php /** * Handle cron events. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\RemoteInboxNotifications\DataSourcePoller; use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine; use Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct; use Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme; use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved; use Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks; use Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog; use Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; use Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use Automattic\WooCommerce\Internal\Admin\Notes\ManageOrdersOnTheGo; use Automattic\WooCommerce\Internal\Admin\Notes\MarketingJetpack; use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify; use Automattic\WooCommerce\Internal\Admin\Notes\MobileApp; use Automattic\WooCommerce\Internal\Admin\Notes\NewSalesRecord; use Automattic\WooCommerce\Internal\Admin\Notes\OnboardingPayments; use Automattic\WooCommerce\Internal\Admin\Notes\OnlineClothingStore; use Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones; use Automattic\WooCommerce\Internal\Admin\Notes\PaymentsMoreInfoNeeded; use Automattic\WooCommerce\Internal\Admin\Notes\PaymentsRemindMeLater; use Automattic\WooCommerce\Internal\Admin\Notes\PerformanceOnMobile; use Automattic\WooCommerce\Internal\Admin\Notes\PersonalizeStore; use Automattic\WooCommerce\Internal\Admin\Notes\RealTimeOrderAlerts; use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn; use Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles; use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments; use Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions; use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes; use Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller; use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\RemoteFreeExtensionsDataSourcePoller; /** * Events Class. */ class Events { /** * The single instance of the class. * * @var object */ protected static $instance = null; /** * Constructor * * @return void */ protected function __construct() {} /** * Array of note class to be added or updated. * * @var array */ private static $note_classes_to_added_or_updated = array( AddFirstProduct::class, ChoosingTheme::class, CustomizeStoreWithBlocks::class, CustomizingProductCatalog::class, EditProductsOnTheMove::class, EUVATNumber::class, FirstProduct::class, LaunchChecklist::class, MagentoMigration::class, ManageOrdersOnTheGo::class, MarketingJetpack::class, MigrateFromShopify::class, MobileApp::class, NewSalesRecord::class, OnboardingPayments::class, OnlineClothingStore::class, PaymentsMoreInfoNeeded::class, PaymentsRemindMeLater::class, PerformanceOnMobile::class, PersonalizeStore::class, RealTimeOrderAlerts::class, TrackingOptIn::class, WooCommercePayments::class, WooCommerceSubscriptions::class, ); /** * The other note classes that are added in other places. * * @var array */ private static $other_note_classes = array( CouponPageMoved::class, InstallJPAndWCSPlugins::class, OrderMilestones::class, SellingOnlineCourses::class, UnsecuredReportFiles::class, WooSubscriptionsNotes::class, ); /** * Get class instance. * * @return object Instance. */ final public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Cron event handlers. */ public function init() { add_action( 'wc_admin_daily', array( $this, 'do_wc_admin_daily' ) ); add_filter( 'woocommerce_get_note_from_db', array( $this, 'get_note_from_db' ), 10, 1 ); // Initialize the WC_Notes_Refund_Returns Note to attach hook. \WC_Notes_Refund_Returns::init(); } /** * Daily events to run. * * Note: Order_Milestones::possibly_add_note is hooked to this as well. */ public function do_wc_admin_daily() { $this->possibly_add_notes(); $this->possibly_delete_notes(); $this->possibly_update_notes(); $this->possibly_refresh_data_source_pollers(); if ( $this->is_remote_inbox_notifications_enabled() ) { 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 <?php /** * Survey helper methods. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * Survey Class. */ class Survey { /** * Survey URL. */ const SURVEY_URL = 'https://automattic.survey.fm'; /** * Get a survey's URL from a path. * * @param string $path Path of the survey. * @param array $query Query arguments as key value pairs. * @return string Full URL to survey. */ public static function get_url( $path, $query = array() ) { $url = self::SURVEY_URL . $path; $query_args = apply_filters( 'woocommerce_admin_survey_query', $query ); if ( ! empty( $query_args ) ) { $query_string = http_build_query( $query_args ); $url = $url . '?' . $query_string; } return $url; } } CategoryLookup.php 0000644 00000017764 15073235522 0010247 0 ustar 00 <?php /** * Keeps the product category lookup table in sync with live data. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * \Automattic\WooCommerce\Internal\Admin\CategoryLookup class. */ class CategoryLookup { /** * Stores changes to categories we need to sync. * * @var array */ protected $edited_product_cats = array(); /** * The single instance of the class. * * @var object */ protected static $instance = null; /** * Constructor * * @return void */ protected function __construct() {} /** * Get class instance. * * @return object Instance. */ final public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init hooks. */ public function init() { add_action( 'generate_category_lookup_table', array( $this, 'regenerate' ) ); add_action( 'edit_product_cat', array( $this, 'before_edit' ), 99 ); add_action( 'edited_product_cat', array( $this, 'on_edit' ), 99 ); add_action( 'created_product_cat', array( $this, 'on_create' ), 99 ); add_action( 'init', array( $this, 'define_category_lookup_tables_in_wpdb' ) ); } /** * Regenerate all lookup table data. */ public function regenerate() { global $wpdb; $wpdb->query( "TRUNCATE TABLE $wpdb->wc_category_lookup" ); $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'id=>parent', ) ); $hierarchy = array(); $inserts = array(); $this->unflatten_terms( $hierarchy, $terms, 0 ); $this->get_term_insert_values( $inserts, $hierarchy ); if ( ! $inserts ) { return; } $insert_string = implode( '),(', array_map( function( $item ) { return implode( ',', $item ); }, $inserts ) ); $wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_tree_id,category_id) VALUES ({$insert_string})" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Store edits so we know when the parent ID changes. * * @param int $category_id Term ID being edited. */ public function before_edit( $category_id ) { $category = get_term( $category_id, 'product_cat' ); $this->edited_product_cats[ $category_id ] = $category->parent; } /** * When a product category gets edited, see if we need to sync the table. * * @param int $category_id Term ID being edited. */ public function on_edit( $category_id ) { global $wpdb; if ( ! isset( $this->edited_product_cats[ $category_id ] ) ) { return; } $category_object = get_term( $category_id, 'product_cat' ); $prev_parent = $this->edited_product_cats[ $category_id ]; $new_parent = $category_object->parent; // No edits - no need to modify relationships. if ( $prev_parent === $new_parent ) { return; } $this->delete( $category_id, $prev_parent ); $this->update( $category_id ); } /** * When a product category gets created, add a new lookup row. * * @param int $category_id Term ID being created. */ public function on_create( $category_id ) { // If WooCommerce is being installed on a multisite, lookup tables haven't been created yet. if ( 'yes' === get_transient( 'wc_installing' ) ) { return; } $this->update( $category_id ); } /** * Delete lookup table data from a tree. * * @param int $category_id Category ID to delete. * @param int $category_tree_id Tree to delete from. * @return void */ protected function delete( $category_id, $category_tree_id ) { global $wpdb; if ( ! $category_tree_id ) { return; } $ancestors = get_ancestors( $category_tree_id, 'product_cat', 'taxonomy' ); $ancestors[] = $category_tree_id; $children = get_term_children( $category_id, 'product_cat' ); $children[] = $category_id; $id_list = implode( ',', array_map( 'intval', array_unique( array_filter( $children ) ) ) ); foreach ( $ancestors as $ancestor ) { $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d AND category_id IN ({$id_list})", $ancestor ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } /** * Updates lookup table data for a category by ID. * * @param int $category_id Category ID to update. */ protected function update( $category_id ) { global $wpdb; $ancestors = get_ancestors( $category_id, 'product_cat', 'taxonomy' ); $children = get_term_children( $category_id, 'product_cat' ); $inserts = array(); $inserts[] = $this->get_insert_sql( $category_id, $category_id ); $children_ids = array_map( 'intval', array_unique( array_filter( $children ) ) ); foreach ( $ancestors as $ancestor ) { $inserts[] = $this->get_insert_sql( $category_id, $ancestor ); foreach ( $children_ids as $child_category_id ) { $inserts[] = $this->get_insert_sql( $child_category_id, $ancestor ); } } $insert_string = implode( ',', $inserts ); $wpdb->query( "INSERT IGNORE INTO $wpdb->wc_category_lookup (category_id, category_tree_id) VALUES {$insert_string}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Get category lookup table values to insert. * * @param int $category_id Category ID to insert. * @param int $category_tree_id Tree to insert into. * @return string */ protected function get_insert_sql( $category_id, $category_tree_id ) { global $wpdb; return $wpdb->prepare( '(%d,%d)', $category_id, $category_tree_id ); } /** * Used to construct insert query recursively. * * @param array $inserts Array of data to insert. * @param array $terms Terms to insert. * @param array $parents Parent IDs the terms belong to. */ protected function get_term_insert_values( &$inserts, $terms, $parents = array() ) { foreach ( $terms as $term ) { $insert_parents = array_merge( array( $term['term_id'] ), $parents ); foreach ( $insert_parents as $parent ) { $inserts[] = array( $parent, $term['term_id'], ); } $this->get_term_insert_values( $inserts, $term['descendants'], $insert_parents ); } } /** * Convert flat terms array into nested array. * * @param array $hierarchy Array to put terms into. * @param array $terms Array of terms (id=>parent). * @param integer $parent Parent ID. */ protected function unflatten_terms( &$hierarchy, &$terms, $parent = 0 ) { foreach ( $terms as $term_id => $parent_id ) { if ( (int) $parent_id === $parent ) { $hierarchy[ $term_id ] = array( 'term_id' => $term_id, 'descendants' => array(), ); unset( $terms[ $term_id ] ); } } foreach ( $hierarchy as $term_id => $terms_array ) { $this->unflatten_terms( $hierarchy[ $term_id ]['descendants'], $terms, $term_id ); } } /** * Get category descendants. * * @param int $category_id The category ID to lookup. * @return array */ protected function get_descendants( $category_id ) { global $wpdb; return wp_parse_id_list( $wpdb->get_col( $wpdb->prepare( "SELECT category_id FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d", $category_id ) ) ); } /** * Return all ancestor category ids for a category. * * @param int $category_id The category ID to lookup. * @return array */ protected function get_ancestors( $category_id ) { global $wpdb; return wp_parse_id_list( $wpdb->get_col( $wpdb->prepare( "SELECT category_tree_id FROM $wpdb->wc_category_lookup WHERE category_id = %d", $category_id ) ) ); } /** * Add category lookup table to $wpdb object. */ public static function define_category_lookup_tables_in_wpdb() { global $wpdb; // List of tables without prefixes. $tables = array( 'wc_category_lookup' => 'wc_category_lookup', ); foreach ( $tables as $name => $table ) { $wpdb->$name = $wpdb->prefix . $table; $wpdb->tables[] = $table; } } } Loader.php 0000644 00000046365 15073235522 0006505 0 ustar 00 <?php /** * Register the scripts, styles, and includes needed for pieces of the WooCommerce Admin experience. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\PluginsHelper; use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController; use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews; use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides; use Automattic\WooCommerce\Internal\Admin\Settings; /** * Loader Class. */ class Loader { /** * Class instance. * * @var Loader instance */ protected static $instance = null; /** * An array of classes to load from the includes folder. * * @var array */ protected static $classes = array(); /** * WordPress capability required to use analytics features. * * @var string */ protected static $required_capability = null; /** * An array of dependencies that have been preloaded (to avoid duplicates). * * @var array */ protected $preloaded_dependencies = array( 'script' => array(), 'style' => array(), ); /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin. */ public function __construct() { Features::get_instance(); WCAdminSharedSettings::get_instance(); Translations::get_instance(); WCAdminUser::get_instance(); Settings::get_instance(); SiteHealth::get_instance(); SystemStatusReport::get_instance(); wc_get_container()->get( Reviews::class ); wc_get_container()->get( ReviewsCommentsOverrides::class ); 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 ); ?> <div id="woocommerce-embedded-root" class="is-embed-loading"> <div class="woocommerce-layout"> <div class="woocommerce-layout__header is-embed-loading"> <h1 class="woocommerce-layout__header-heading"> <?php self::output_heading( end( $sections ) ); ?> </h1> </div> </div> </div> <?php } /** * Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios. * * @param string $admin_body_class Body class to add. */ public static function add_admin_body_classes( $admin_body_class = '' ) { if ( ! PageController::is_admin_or_embed_page() ) { return $admin_body_class; } $classes = explode( ' ', trim( $admin_body_class ) ); $classes[] = 'woocommerce-admin-page'; if ( PageController::is_embed_page() ) { $classes[] = 'woocommerce-embed-page'; } /** * Some routes or features like onboarding hide the wp-admin navigation and masterbar. * Setting `woocommerce_admin_is_loading` to true allows us to premeptively hide these * elements while the JS app loads. * This class needs to be removed by those feature components (like <ProfileWizard />). * * @param bool $is_loading If WooCommerce Admin is loading a fullscreen view. */ $is_loading = apply_filters( 'woocommerce_admin_is_loading', false ); if ( PageController::is_admin_page() && $is_loading ) { $classes[] = 'woocommerce-admin-is-loading'; } $admin_body_class = implode( ' ', array_unique( $classes ) ); return " $admin_body_class "; } /** * Adds an iOS "Smart App Banner" for display on iOS Safari. * See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html */ public static function smart_app_banner() { if ( PageController::is_admin_or_embed_page() ) { echo " <meta name='apple-itunes-app' content='app-id=1389130815'> "; } } /** * Removes notices that should not be displayed on WC Admin pages. */ public static function remove_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // Hello Dolly. if ( function_exists( 'hello_dolly' ) ) { remove_action( 'admin_notices', 'hello_dolly' ); } } /** * Runs before admin notices action and hides them. */ public static function inject_before_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // The JITMs won't be shown in the Onboarding Wizard. $is_onboarding = isset( $_GET['path'] ) && '/setup-wizard' === wc_clean( wp_unslash( $_GET['path'] ) ); // phpcs:ignore WordPress.Security.NonceVerification $maybe_hide_jitm = $is_onboarding ? '-hide' : ''; echo '<div class="woocommerce-layout__jitm' . sanitize_html_class( $maybe_hide_jitm ) . '" id="jp-admin-notices"></div>'; // Wrap the notices in a hidden div to prevent flickering before // they are moved elsewhere in the page by WordPress Core. echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">'; if ( PageController::is_admin_page() ) { // Capture all notices and hide them. WordPress Core looks for // `.wp-header-end` and appends notices after it if found. // https://github.com/WordPress/WordPress/blob/f6a37e7d39e2534d05b9e542045174498edfe536/wp-admin/js/common.js#L737 . echo '<div class="wp-header-end" id="woocommerce-layout__notice-catcher"></div>'; } } /** * Runs after admin notices and closes div. */ public static function inject_after_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // Close the hidden div used to prevent notices from flickering before // they are inserted elsewhere in the page. echo '</div>'; } /** * Edits Admin title based on section of wc-admin. * * @param string $admin_title Modifies admin title. * @todo Can we do some URL rewriting so we can figure out which page they are on server side? */ public static function update_admin_title( $admin_title ) { if ( ! did_action( 'current_screen' ) || ! PageController::is_admin_page() ) { return $admin_title; } $sections = self::get_embed_breadcrumbs(); $pieces = array(); foreach ( $sections as $section ) { $pieces[] = is_array( $section ) ? $section[1] : $section; } $pieces = array_reverse( $pieces ); $title = implode( ' ‹ ', $pieces ); /* translators: %1$s: updated title, %2$s: blog info name */ return sprintf( __( '%1$s ‹ %2$s', 'woocommerce' ), $title, get_bloginfo( 'name' ) ); } /** * Set up a div for the app to render into. */ public static function page_wrapper() { ?> <div class="wrap"> <div id="root"></div> </div> <?php } /** * Hooks extra necessary data into the component settings array already set in WooCommerce core. * * @param array $settings Array of component settings. * @return array Array of component settings. */ public static function add_component_settings( $settings ) { if ( ! is_admin() ) { return $settings; } if ( ! function_exists( 'wc_blocks_container' ) ) { global $wp_locale; // inject data not available via older versions of wc_blocks/woo. $settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() ); $settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() ); $settings['currency'] = self::get_currency_settings(); $settings['locale'] = [ 'siteLocale' => isset( $settings['siteLocale'] ) ? $settings['siteLocale'] : get_locale(), 'userLocale' => isset( $settings['l10n']['userLocale'] ) ? $settings['l10n']['userLocale'] : get_user_locale(), 'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] ) ? $settings['l10n']['weekdaysShort'] : array_values( $wp_locale->weekday_abbrev ), ]; } $preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() ); $preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection'; if ( ! empty( $preload_data_endpoints ) ) { $preload_data = array_reduce( array_values( $preload_data_endpoints ), 'rest_preload_api_request' ); } $preload_options = apply_filters( 'woocommerce_admin_preload_options', array() ); if ( ! empty( $preload_options ) ) { foreach ( $preload_options as $option ) { $settings['preloadOptions'][ $option ] = get_option( $option ); } } $preload_settings = apply_filters( 'woocommerce_admin_preload_settings', array() ); if ( ! empty( $preload_settings ) ) { $setting_options = new \WC_REST_Setting_Options_V2_Controller(); foreach ( $preload_settings as $group ) { $group_settings = $setting_options->get_group_settings( $group ); $preload_settings = []; foreach ( $group_settings as $option ) { if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) { $preload_settings[ $option['id'] ] = $option['value']; } } $settings['preloadSettings'][ $group ] = $preload_settings; } } $user_controller = new \WP_REST_Users_Controller(); $request = new \WP_REST_Request(); $request->set_query_params( array( 'context' => 'edit' ) ); $user_response = $user_controller->get_current_item( $request ); $current_user_data = is_wp_error( $user_response ) ? (object) array() : $user_response->get_data(); $settings['currentUserData'] = $current_user_data; $settings['reviewsEnabled'] = get_option( 'woocommerce_enable_reviews' ); $settings['manageStock'] = get_option( 'woocommerce_manage_stock' ); $settings['commentModeration'] = get_option( 'comment_moderation' ); $settings['notifyLowStockAmount'] = get_option( 'woocommerce_notify_low_stock_amount' ); // @todo On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired, // and `wcAssetUrl` can be used in its place throughout the codebase. $settings['wcAdminAssetUrl'] = WC_ADMIN_IMAGES_FOLDER_URL; $settings['wcVersion'] = WC_VERSION; $settings['siteUrl'] = site_url(); $settings['shopUrl'] = get_permalink( wc_get_page_id( 'shop' ) ); $settings['homeUrl'] = home_url(); $settings['dateFormat'] = get_option( 'date_format' ); $settings['timeZone'] = wc_timezone_string(); $settings['plugins'] = array( 'installedPlugins' => PluginsHelper::get_installed_plugin_slugs(), 'activePlugins' => Plugins::get_active_plugins(), ); // Plugins that depend on changing the translation work on the server but not the client - // WooCommerce Branding is an example of this - so pass through the translation of // 'WooCommerce' to wcSettings. $settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce' ); // We may have synced orders with a now-unregistered status. // E.g An extension that added statuses is now inactive or removed. $settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses(); // The separator used for attributes found in Variation titles. $settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() ); if ( ! empty( $preload_data_endpoints ) ) { $settings['dataEndpoints'] = isset( $settings['dataEndpoints'] ) ? $settings['dataEndpoints'] : []; foreach ( $preload_data_endpoints as $key => $endpoint ) { // Handle error case: rest_do_request() doesn't guarantee success. if ( empty( $preload_data[ $endpoint ] ) ) { $settings['dataEndpoints'][ $key ] = array(); } else { $settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body']; } } } $settings = self::get_custom_settings( $settings ); if ( PageController::is_embed_page() ) { $settings['embedBreadcrumbs'] = self::get_embed_breadcrumbs(); } $settings['allowMarketplaceSuggestions'] = WC_Marketplace_Suggestions::allow_suggestions(); $settings['connectNonce'] = wp_create_nonce( 'connect' ); $settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' ); return $settings; } /** * Format order statuses by removing a leading 'wc-' if present. * * @param array $statuses Order statuses. * @return array formatted statuses. */ public static function get_order_statuses( $statuses ) { $formatted_statuses = array(); foreach ( $statuses as $key => $value ) { $formatted_key = preg_replace( '/^wc-/', '', $key ); $formatted_statuses[ $formatted_key ] = $value; } return $formatted_statuses; } /** * Get all order statuses present in analytics tables that aren't registered. * * @return array Unregistered order statuses. */ public static function get_unregistered_order_statuses() { $registered_statuses = wc_get_order_statuses(); $all_synced_statuses = OrdersDataStore::get_all_statuses(); $unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) ); $formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) ); $formatted_statuses = array_keys( $formatted_status_keys ); return array_combine( $formatted_statuses, $formatted_statuses ); } /** * Register the admin settings for use in the WC REST API * * @param array $groups Array of setting groups. * @return array */ public static function add_settings_group( $groups ) { $groups[] = array( 'id' => 'wc_admin', 'label' => __( 'WooCommerce Admin', 'woocommerce' ), 'description' => __( 'Settings for WooCommerce admin reporting.', 'woocommerce' ), ); return $groups; } /** * Add WC Admin specific settings * * @param array $settings Array of settings in wc admin group. * @return array */ public static function add_settings( $settings ) { $unregistered_statuses = self::get_unregistered_order_statuses(); $registered_statuses = self::get_order_statuses( wc_get_order_statuses() ); $all_statuses = array_merge( $unregistered_statuses, $registered_statuses ); $settings[] = array( 'id' => 'woocommerce_excluded_report_order_statuses', 'option_key' => 'woocommerce_excluded_report_order_statuses', 'label' => __( 'Excluded report order statuses', 'woocommerce' ), 'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce' ), 'default' => array( 'pending', 'cancelled', 'failed' ), 'type' => 'multiselect', 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_actionable_order_statuses', 'option_key' => 'woocommerce_actionable_order_statuses', 'label' => __( 'Actionable order statuses', 'woocommerce' ), 'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce' ), 'default' => array( 'processing', 'on-hold' ), 'type' => 'multiselect', 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_default_date_range', 'option_key' => 'woocommerce_default_date_range', 'label' => __( 'Default Date Range', 'woocommerce' ), 'description' => __( 'Default Date Range', 'woocommerce' ), 'default' => 'period=month&compare=previous_year', 'type' => 'text', ); return $settings; } /** * Gets custom settings used for WC Admin. * * @param array $settings Array of settings to merge into. * @return array */ public static function get_custom_settings( $settings ) { $wc_rest_settings_options_controller = new \WC_REST_Setting_Options_Controller(); $wc_admin_group_settings = $wc_rest_settings_options_controller->get_group_settings( 'wc_admin' ); $settings['wcAdminSettings'] = array(); foreach ( $wc_admin_group_settings as $setting ) { if ( ! empty( $setting['id'] ) ) { $settings['wcAdminSettings'][ $setting['id'] ] = $setting['value']; } } return $settings; } /** * Return an object defining the currecy options for the site's current currency * * @return array Settings for the current currency { * Array of settings. * * @type string $code Currency code. * @type string $precision Number of decimals. * @type string $symbol Symbol for currency. * } */ public static function get_currency_settings() { $code = get_woocommerce_currency(); return apply_filters( 'wc_currency_settings', array( 'code' => $code, 'precision' => wc_get_price_decimals(), 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ), 'symbolPosition' => get_option( 'woocommerce_currency_pos' ), 'decimalSeparator' => wc_get_price_decimal_separator(), 'thousandSeparator' => wc_get_price_thousand_separator(), 'priceFormat' => html_entity_decode( get_woocommerce_price_format() ), ) ); } /** * Delete woocommerce_onboarding_homepage_post_id field when the homepage is deleted * * @param int $post_id The deleted post id. */ public static function delete_homepage( $post_id ) { if ( 'page' !== get_post_type( $post_id ) ) { return; } $homepage_id = intval( get_option( 'woocommerce_onboarding_homepage_post_id', false ) ); if ( $homepage_id === $post_id ) { delete_option( 'woocommerce_onboarding_homepage_post_id' ); } } /** * Adds the appearance_theme_view Tracks event. */ public static function add_appearance_theme_view_tracks_event() { wc_admin_record_tracks_event( 'appearance_theme_view', array() ); } } ShippingLabelBanner.php 0000644 00000010226 15073235522 0011131 0 ustar 00 <?php /** * WooCommerce Shipping Label banner. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager; /** * Shows print shipping label banner on edit order page. */ class ShippingLabelBanner { /** * Singleton for the display rules class * * @var ShippingLabelBannerDisplayRules */ private $shipping_label_banner_display_rules; /** * Constructor */ public function __construct() { if ( ! is_admin() ) { return; } add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 6, 2 ); } /** * Check if WooCommerce Shipping makes sense for this merchant. * * @return bool */ private function should_show_meta_box() { if ( ! $this->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 ) { ?> <div id="wc-admin-shipping-banner-root" class="woocommerce <?php echo esc_attr( 'wc-admin-shipping-banner' ); ?>" data-args="<?php echo esc_attr( wp_json_encode( $args['args'] ) ); ?>"> </div> <?php } } Onboarding/OnboardingHelper.php 0000644 00000011502 15073235522 0012564 0 ustar 00 <?php /** * WooCommerce Onboarding Helper */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; /** * Contains backend logic for the onboarding profile and checklist feature. */ class OnboardingHelper { /** * Class instance. * * @var OnboardingHelper instance */ private static $instance = null; /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init. */ public function init() { if ( ! is_admin() ) { return; } add_action( 'current_screen', array( $this, 'add_help_tab' ), 60 ); add_action( 'current_screen', array( $this, 'reset_task_list' ) ); add_action( 'current_screen', array( $this, 'reset_extended_task_list' ) ); } /** * Update the help tab setup link to reset the onboarding profiler. */ public function add_help_tab() { if ( ! function_exists( 'wc_get_screen_ids' ) ) { return; } $screen = get_current_screen(); if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids(), true ) ) { return; } // Remove the old help tab if it exists. $help_tabs = $screen->get_help_tabs(); foreach ( $help_tabs as $help_tab ) { if ( 'woocommerce_onboard_tab' !== $help_tab['id'] ) { continue; } $screen->remove_help_tab( 'woocommerce_onboard_tab' ); } // Add the new help tab. $help_tab = array( 'title' => __( 'Setup wizard', 'woocommerce' ), 'id' => 'woocommerce_onboard_tab', ); $setup_list = TaskLists::get_list( 'setup' ); $extended_list = TaskLists::get_list( 'extended' ); if ( $setup_list ) { $help_tab['content'] = '<h2>' . __( 'WooCommerce Onboarding', 'woocommerce' ) . '</h2>'; $help_tab['content'] .= '<h3>' . __( 'Profile Setup Wizard', 'woocommerce' ) . '</h3>'; $help_tab['content'] .= '<p>' . __( 'If you need to access the setup wizard again, please click on the button below.', 'woocommerce' ) . '</p>' . '<p><a href="' . wc_admin_url( '&path=/setup-wizard' ) . '" class="button button-primary">' . __( 'Setup wizard', 'woocommerce' ) . '</a></p>'; $help_tab['content'] .= '<h3>' . __( 'Task List', 'woocommerce' ) . '</h3>'; $help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the task lists, please click on the button below.', 'woocommerce' ) . '</p>' . ( $setup_list->is_hidden() ? '<p><a href="' . wc_admin_url( '&reset_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>' : '<p><a href="' . wc_admin_url( '&reset_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>' ); } if ( $extended_list ) { $help_tab['content'] .= '<h3>' . __( 'Extended task List', 'woocommerce' ) . '</h3>'; $help_tab['content'] .= '<p>' . __( 'If you need to enable or disable the extended task lists, please click on the button below.', 'woocommerce' ) . '</p>' . ( $extended_list->is_hidden() ? '<p><a href="' . wc_admin_url( '&reset_extended_task_list=1' ) . '" class="button button-primary">' . __( 'Enable', 'woocommerce' ) . '</a></p>' : '<p><a href="' . wc_admin_url( '&reset_extended_task_list=0' ) . '" class="button button-primary">' . __( 'Disable', 'woocommerce' ) . '</a></p>' ); } $screen->add_help_tab( $help_tab ); } /** * Reset the onboarding task list and redirect to the dashboard. */ public function reset_task_list() { if ( ! PageController::is_admin_page() || ! isset( $_GET['reset_task_list'] ) // phpcs:ignore CSRF ok. ) { return; } $task_list = TaskLists::get_list( 'setup' ); if ( ! $task_list ) { return; } $show = 1 === absint( $_GET['reset_task_list'] ); // phpcs:ignore CSRF ok. $update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok. if ( $update ) { wc_admin_record_tracks_event( 'tasklist_toggled', array( 'status' => $show ? 'enabled' : 'disabled', ) ); } wp_safe_redirect( wc_admin_url() ); exit; } /** * Reset the extended task list and redirect to the dashboard. */ public function reset_extended_task_list() { if ( ! PageController::is_admin_page() || ! isset( $_GET['reset_extended_task_list'] ) // phpcs:ignore CSRF ok. ) { return; } $task_list = TaskLists::get_list( 'extended' ); if ( ! $task_list ) { return; } $show = 1 === absint( $_GET['reset_extended_task_list'] ); // phpcs:ignore CSRF ok. $update = $show ? $task_list->unhide() : $task_list->hide(); // phpcs:ignore CSRF ok. if ( $update ) { wc_admin_record_tracks_event( 'extended_tasklist_toggled', array( 'status' => $show ? 'disabled' : 'enabled', ) ); } wp_safe_redirect( wc_admin_url() ); exit; } } Onboarding/OnboardingProfile.php 0000644 00000003564 15073235522 0012756 0 ustar 00 <?php /** * WooCommerce Onboarding Setup Wizard */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\WCAdminHelper; /** * Contains backend logic for the onboarding profile and checklist feature. */ class OnboardingProfile { /** * Profile data option name. */ const DATA_OPTION = 'woocommerce_onboarding_profile'; /** * Add onboarding actions. */ public static function init() { add_action( 'update_option_' . self::DATA_OPTION, array( __CLASS__, 'trigger_complete' ), 10, 2 ); } /** * Trigger the woocommerce_onboarding_profile_completed action * * @param array $old_value Previous value. * @param array $value Current value. */ public static function trigger_complete( $old_value, $value ) { if ( isset( $old_value['completed'] ) && $old_value['completed'] ) { return; } if ( ! isset( $value['completed'] ) || ! $value['completed'] ) { return; } /** * Action hook fired when the onboarding profile (or onboarding wizard, * or profiler) is completed. * * @since 1.5.0 */ do_action( 'woocommerce_onboarding_profile_completed' ); } /** * Check if the profiler still needs to be completed. * * @return bool */ public static function needs_completion() { $onboarding_data = get_option( self::DATA_OPTION, array() ); $is_completed = isset( $onboarding_data['completed'] ) && true === $onboarding_data['completed']; $is_skipped = isset( $onboarding_data['skipped'] ) && true === $onboarding_data['skipped']; // @todo When merging to WooCommerce Core, we should set the `completed` flag to true during the upgrade progress. // https://github.com/woocommerce/woocommerce-admin/pull/2300#discussion_r287237498. return ! $is_completed && ! $is_skipped; } } Onboarding/OnboardingIndustries.php 0000644 00000005020 15073235522 0013474 0 ustar 00 <?php /** * WooCommerce Onboarding Industries */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; /** * Logic around onboarding industries. */ class OnboardingIndustries { /** * Init. */ public static function init() { add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) ); } /** * Get a list of allowed industries for the onboarding wizard. * * @return array */ public static function get_allowed_industries() { /* With "use_description" we turn the description input on. With "description_label" we set the input label */ return apply_filters( 'woocommerce_admin_onboarding_industries', array( 'fashion-apparel-accessories' => array( 'label' => __( 'Fashion, apparel, and accessories', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'health-beauty' => array( 'label' => __( 'Health and beauty', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'electronics-computers' => array( 'label' => __( 'Electronics and computers', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'food-drink' => array( 'label' => __( 'Food and drink', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'home-furniture-garden' => array( 'label' => __( 'Home, furniture, and garden', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'cbd-other-hemp-derived-products' => array( 'label' => __( 'CBD and other hemp-derived products', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'education-and-learning' => array( 'label' => __( 'Education and learning', 'woocommerce' ), 'use_description' => false, 'description_label' => '', ), 'other' => array( 'label' => __( 'Other', 'woocommerce' ), 'use_description' => true, 'description_label' => __( 'Description', 'woocommerce' ), ), ) ); } /** * Add preloaded data to onboarding. * * @param array $settings Component settings. * @return array */ public static function preload_data( $settings ) { $settings['onboarding']['industries'] = self::get_allowed_industries(); return $settings; } } Onboarding/OnboardingMailchimp.php 0000644 00000002301 15073235522 0013245 0 ustar 00 <?php /** * WooCommerce Onboarding Mailchimp */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Internal\Admin\Schedulers\MailchimpScheduler; /** * Logic around updating Mailchimp during onboarding. */ class OnboardingMailchimp { /** * Class instance. * * @var OnboardingMailchimp instance */ private static $instance = null; /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init. */ public function init() { add_action( 'woocommerce_onboarding_profile_data_updated', array( $this, 'on_profile_data_updated' ), 10, 2 ); } /** * Reset MailchimpScheduler if profile data is being updated with a new email. * * @param array $existing_data Existing option data. * @param array $updating_data Updating option data. */ public function on_profile_data_updated( $existing_data, $updating_data ) { if ( isset( $existing_data['store_email'] ) && isset( $updating_data['store_email'] ) && $existing_data['store_email'] !== $updating_data['store_email'] ) { MailchimpScheduler::reset(); } } } Onboarding/OnboardingSetupWizard.php 0000644 00000020721 15073235522 0013631 0 ustar 00 <?php /** * WooCommerce Onboarding Setup Wizard */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\WCAdminHelper; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; /** * Contains backend logic for the onboarding profile and checklist feature. */ class OnboardingSetupWizard { /** * Class instance. * * @var OnboardingSetupWizard instance */ private static $instance = null; /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Add onboarding actions. */ public function init() { if ( ! is_admin() ) { return; } // Old settings injection. // Run after Automattic\WooCommerce\Internal\Admin\Loader. add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // New settings injection. add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 20 ); add_filter( 'woocommerce_admin_preload_settings', array( $this, 'preload_settings' ) ); add_filter( 'admin_body_class', array( $this, 'add_loading_classes' ) ); add_action( 'admin_init', array( $this, 'do_admin_redirects' ) ); add_action( 'current_screen', array( $this, 'redirect_to_profiler' ) ); add_filter( 'woocommerce_show_admin_notice', array( $this, 'remove_old_install_notice' ), 10, 2 ); } /** * Test whether the context of execution comes from async action scheduler. * Note: this is a polyfill for wc_is_running_from_async_action_scheduler() * which was introduced in WC 4.0. * * @return bool */ private function is_running_from_async_action_scheduler() { if ( function_exists( '\wc_is_running_from_async_action_scheduler' ) ) { return \wc_is_running_from_async_action_scheduler(); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action']; } /** * Handle redirects to setup/welcome page after install and updates. * * For setup wizard, transient must be present, the user must have access rights, and we must ignore the network/bulk plugin updaters. */ public function do_admin_redirects() { // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. // That means OBW would never be shown. if ( $this->is_running_from_async_action_scheduler() ) { return; } // Setup wizard redirect. if ( get_transient( '_wc_activation_redirect' ) && apply_filters( 'woocommerce_enable_setup_wizard', true ) ) { $do_redirect = true; $current_page = isset( $_GET['page'] ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification $is_onboarding_path = ! isset( $_GET['path'] ) || '/setup-wizard' === wc_clean( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification // On these pages, or during these events, postpone the redirect. if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_woocommerce' ) ) { $do_redirect = false; } // On these pages, or during these events, disable the redirect. if ( ( 'wc-admin' === $current_page && $is_onboarding_path ) || apply_filters( 'woocommerce_prevent_automatic_wizard_redirect', false ) || isset( $_GET['activate-multi'] ) // phpcs:ignore WordPress.Security.NonceVerification ) { delete_transient( '_wc_activation_redirect' ); $do_redirect = false; } if ( $do_redirect ) { delete_transient( '_wc_activation_redirect' ); wp_safe_redirect( wc_admin_url() ); exit; } } } /** * Trigger the woocommerce_onboarding_profile_completed action * * @param array $old_value Previous value. * @param array $value Current value. */ public function trigger_profile_completed_action( $old_value, $value ) { if ( isset( $old_value['completed'] ) && $old_value['completed'] ) { return; } if ( ! isset( $value['completed'] ) || ! $value['completed'] ) { return; } /** * Action hook fired when the onboarding profile (or onboarding wizard, * or profiler) is completed. * * @since 1.5.0 */ do_action( 'woocommerce_onboarding_profile_completed' ); } /** * Returns true if the profiler should be displayed (not completed and not skipped). * * @return bool */ private function should_show() { if ( $this->is_setup_wizard() ) { return true; } return OnboardingProfile::needs_completion(); } /** * Redirect to the profiler on homepage if completion is needed. */ public function redirect_to_profiler() { if ( ! $this->is_homepage() || ! OnboardingProfile::needs_completion() ) { return; } wp_safe_redirect( wc_admin_url( '&path=/setup-wizard' ) ); exit; } /** * Check if the current page is the profile wizard. * * @return bool */ private function is_setup_wizard() { /* phpcs:disable WordPress.Security.NonceVerification */ return isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] && isset( $_GET['path'] ) && '/setup-wizard' === $_GET['path']; /* phpcs: enable */ } /** * Check if the current page is the homepage. * * @return bool */ private function is_homepage() { /* phpcs:disable WordPress.Security.NonceVerification */ return isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] && ! isset( $_GET['path'] ); /* phpcs: enable */ } /** * Determine if the current page is one of the WC Admin pages. * * @return bool */ private function is_woocommerce_page() { $current_page = PageController::get_instance()->get_current_page(); if ( ! $current_page || ! isset( $current_page['path'] ) ) { return false; } return 0 === strpos( $current_page['path'], 'wc-admin' ); } /** * Add profiler items to component settings. * * @param array $settings Component settings. * * @return array */ public function component_settings( $settings ) { $profile = (array) get_option( OnboardingProfile::DATA_OPTION, array() ); $settings['onboarding'] = array( 'profile' => $profile, ); // Only fetch if the onboarding wizard OR the task list is incomplete or currently shown // or the current page is one of the WooCommerce Admin pages. if ( ( ! $this->should_show() && ! count( TaskLists::get_visible() ) || ! $this->is_woocommerce_page() ) ) { return $settings; } include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php'; $wccom_auth = \WC_Helper_Options::get( 'auth' ); $profile['wccom_connected'] = empty( $wccom_auth['access_token'] ) ? false : true; $settings['onboarding']['currencySymbols'] = get_woocommerce_currency_symbols(); $settings['onboarding']['euCountries'] = WC()->countries->get_european_union_countries(); $settings['onboarding']['localeInfo'] = include WC()->plugin_path() . '/i18n/locale-info.php'; $settings['onboarding']['profile'] = $profile; if ( $this->is_setup_wizard() ) { $settings['onboarding']['pageCount'] = (int) ( wp_count_posts( 'page' ) )->publish; $settings['onboarding']['postCount'] = (int) ( wp_count_posts( 'post' ) )->publish; $settings['onboarding']['isBlockTheme'] = wc_current_theme_is_fse_theme(); } return apply_filters( 'woocommerce_admin_onboarding_preloaded_data', $settings ); } /** * Preload WC setting options to prime state of the application. * * @param array $options Array of options to preload. * @return array */ public function preload_settings( $options ) { $options[] = 'general'; return $options; } /** * Set the admin full screen class when loading to prevent flashes of unstyled content. * * @param bool $classes Body classes. * @return array */ public function add_loading_classes( $classes ) { /* phpcs:disable WordPress.Security.NonceVerification */ if ( $this->is_setup_wizard() ) { $classes .= ' woocommerce-admin-full-screen'; } /* phpcs: enable */ return $classes; } /** * Remove the install notice that prompts the user to visit the old onboarding setup wizard. * * @param bool $show Show or hide the notice. * @param string $notice The slug of the notice. * @return bool */ public function remove_old_install_notice( $show, $notice ) { if ( 'install' === $notice ) { return false; } return $show; } } Onboarding/OnboardingSync.php 0000644 00000007701 15073235522 0012267 0 ustar 00 <?php /** * WooCommerce Onboarding */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; /** * Contains backend logic for the onboarding profile and checklist feature. */ class OnboardingSync { /** * Class instance. * * @var OnboardingSync instance */ private static $instance = null; /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init. */ public function init() { add_action( 'update_option_' . OnboardingProfile::DATA_OPTION, array( $this, 'send_profile_data_on_update' ), 10, 2 ); add_action( 'woocommerce_helper_connected', array( $this, 'send_profile_data_on_connect' ) ); if ( ! is_admin() ) { return; } add_action( 'current_screen', array( $this, 'redirect_wccom_install' ) ); } /** * Send profile data to Woo.com. */ private function send_profile_data() { if ( 'yes' !== get_option( 'woocommerce_allow_tracking', 'no' ) ) { return; } if ( ! class_exists( '\WC_Helper_API' ) || ! method_exists( '\WC_Helper_API', 'put' ) ) { return; } if ( ! class_exists( '\WC_Helper_Options' ) ) { return; } $auth = \WC_Helper_Options::get( 'auth' ); if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) { return false; } $profile = get_option( OnboardingProfile::DATA_OPTION, array() ); $base_location = wc_get_base_location(); $defaults = array( 'plugins' => 'skipped', 'industry' => array(), 'product_types' => array(), 'product_count' => '0', 'selling_venues' => 'no', 'number_employees' => '1', 'revenue' => 'none', 'other_platform' => 'none', 'business_extensions' => array(), 'theme' => get_stylesheet(), 'setup_client' => false, 'store_location' => $base_location['country'], 'default_currency' => get_woocommerce_currency(), ); // Prepare industries as an array of slugs if they are in array format. if ( isset( $profile['industry'] ) && is_array( $profile['industry'] ) ) { $industry_slugs = array(); foreach ( $profile['industry'] as $industry ) { $industry_slugs[] = is_array( $industry ) ? $industry['slug'] : $industry; } $profile['industry'] = $industry_slugs; } $body = wp_parse_args( $profile, $defaults ); \WC_Helper_API::put( 'profile', array( 'authenticated' => true, 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', ), ) ); } /** * Send profiler data on profiler change to completion. * * @param array $old_value Previous value. * @param array $value Current value. */ public function send_profile_data_on_update( $old_value, $value ) { if ( ! isset( $value['completed'] ) || ! $value['completed'] ) { return; } $this->send_profile_data(); } /** * Send profiler data after a site is connected. */ public function send_profile_data_on_connect() { $profile = get_option( OnboardingProfile::DATA_OPTION, array() ); if ( ! isset( $profile['completed'] ) || ! $profile['completed'] ) { return; } $this->send_profile_data(); } /** * Redirects the user to the task list if the task list is enabled and finishing a wccom checkout. * * @todo Once URL params are added to the redirect, we can check those instead of the referer. */ public function redirect_wccom_install() { $task_list = TaskLists::get_list( 'setup' ); if ( ! $task_list || $task_list->is_hidden() || ! isset( $_SERVER['HTTP_REFERER'] ) || 0 !== strpos( $_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 <?php /** * WooCommerce Onboarding Themes */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Admin\Loader; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\WCAdminHelper; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Init as OnboardingTasks; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler; /** * Logic around onboarding themes. */ class OnboardingThemes { /** * Name of themes transient. * * @var string */ const THEMES_TRANSIENT = 'wc_onboarding_themes'; /** * Init. */ public static function init() { add_action( 'woocommerce_theme_installed', array( __CLASS__, 'delete_themes_transient' ) ); add_action( 'after_switch_theme', array( __CLASS__, 'delete_themes_transient' ) ); add_filter( 'woocommerce_rest_prepare_themes', array( __CLASS__, 'add_uploaded_theme_data' ) ); add_filter( 'woocommerce_admin_onboarding_preloaded_data', array( __CLASS__, 'preload_data' ) ); } /** * Get puchasable theme by slug. * * @param string $price_string string of price. * @return float|null */ private static function get_price_from_string( $price_string ) { $price_match = null; // Parse price from string as it includes the currency symbol. preg_match( '/\\d+\.\d{2}\s*/', $price_string, $price_match ); if ( count( $price_match ) > 0 ) { return (float) $price_match[0]; } return null; } /** * Get puchasable theme by slug. * * @param string $slug from theme. * @return array|null */ public static function get_paid_theme_by_slug( $slug ) { $themes = self::get_themes(); $theme_key = array_search( $slug, array_column( $themes, 'slug' ), true ); $theme = false !== $theme_key ? $themes[ $theme_key ] : null; if ( $theme && isset( $theme['id'] ) && isset( $theme['price'] ) ) { $price = self::get_price_from_string( $theme['price'] ); if ( $price && $price > 0 ) { return $themes[ $theme_key ]; } } return null; } /** * Sort themes returned from 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 <?php /** * WooCommerce Onboarding */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; /** * Initializes backend logic for the onboarding process. */ class Onboarding { /** * Initialize onboarding functionality. */ public static function init() { OnboardingHelper::instance()->init(); OnboardingIndustries::init(); OnboardingJetpack::instance()->init(); OnboardingMailchimp::instance()->init(); OnboardingProfile::init(); OnboardingSetupWizard::instance()->init(); OnboardingSync::instance()->init(); OnboardingThemes::init(); } } Onboarding/OnboardingProducts.php 0000644 00000012534 15073235522 0013156 0 ustar 00 <?php /** * WooCommerce Onboarding Products */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; use Automattic\WooCommerce\Admin\Loader; use Automattic\WooCommerce\Admin\PluginsHelper; /** * Class for handling product types and data around product types. */ class OnboardingProducts { /** * Name of product data transient. * * @var string */ const PRODUCT_DATA_TRANSIENT = 'wc_onboarding_product_data'; /** * Get a list of allowed product types for the onboarding wizard. * * @return array */ public static function get_allowed_product_types() { $products = array( 'physical' => array( 'label' => __( 'Physical products', 'woocommerce' ), 'default' => true, ), 'downloads' => array( 'label' => __( 'Downloads', 'woocommerce' ), ), 'subscriptions' => array( 'label' => __( 'Subscriptions', 'woocommerce' ), ), 'memberships' => array( 'label' => __( 'Memberships', 'woocommerce' ), 'product' => 958589, ), 'bookings' => array( 'label' => __( 'Bookings', 'woocommerce' ), 'product' => 390890, ), 'product-bundles' => array( 'label' => __( 'Bundles', 'woocommerce' ), 'product' => 18716, ), 'product-add-ons' => array( 'label' => __( 'Customizable products', 'woocommerce' ), 'product' => 18618, ), ); $base_location = wc_get_base_location(); $has_cbd_industry = false; if ( 'US' === $base_location['country'] ) { $profile = get_option( OnboardingProfile::DATA_OPTION, array() ); if ( ! empty( $profile['industry'] ) ) { $has_cbd_industry = in_array( 'cbd-other-hemp-derived-products', array_column( $profile['industry'], 'slug' ), true ); } } if ( ! Features::is_enabled( 'subscriptions' ) || 'US' !== $base_location['country'] || $has_cbd_industry ) { $products['subscriptions']['product'] = 27147; } return apply_filters( 'woocommerce_admin_onboarding_product_types', $products ); } /** * Get dynamic product data from API. * * @param array $product_types Array of product types. * @return array */ public static function get_product_data( $product_types ) { $locale = get_user_locale(); // Transient value is an array of product data keyed by locale. $transient_value = get_transient( self::PRODUCT_DATA_TRANSIENT ); $transient_value = is_array( $transient_value ) ? $transient_value : array(); $woocommerce_products = $transient_value[ $locale ] ?? false; if ( false === $woocommerce_products ) { $woocommerce_products = wp_remote_get( add_query_arg( array( 'locale' => $locale, ), 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' ), array( 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( is_wp_error( $woocommerce_products ) ) { return $product_types; } $transient_value[ $locale ] = $woocommerce_products; set_transient( self::PRODUCT_DATA_TRANSIENT, $transient_value, DAY_IN_SECONDS ); } $data = json_decode( $woocommerce_products['body'] ); $products = array(); $product_data = array(); // Map product data by ID. if ( isset( $data ) && isset( $data->products ) ) { foreach ( $data->products as $product_datum ) { if ( isset( $product_datum->id ) ) { $products[ $product_datum->id ] = $product_datum; } } } // Loop over product types and append data. foreach ( $product_types as $key => $product_type ) { $product_data[ $key ] = $product_types[ $key ]; if ( isset( $product_type['product'] ) && isset( $products[ $product_type['product'] ] ) ) { $price = html_entity_decode( $products[ $product_type['product'] ]->price ); $yearly_price = (float) str_replace( '$', '', $price ); $product_data[ $key ]['yearly_price'] = $yearly_price; $product_data[ $key ]['description'] = $products[ $product_type['product'] ]->excerpt; $product_data[ $key ]['more_url'] = $products[ $product_type['product'] ]->link; $product_data[ $key ]['slug'] = strtolower( preg_replace( '~[^\pL\d]+~u', '-', $products[ $product_type['product'] ]->slug ) ); } } return $product_data; } /** * Get the allowed product types with the polled data. * * @return array */ public static function get_product_types_with_data() { return self::get_product_data( self::get_allowed_product_types() ); } /** * Get relevant purchaseable products for the site. * * @return array */ public static function get_relevant_products() { $profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() ); $installed = PluginsHelper::get_installed_plugin_slugs(); $product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array(); $product_data = self::get_product_types_with_data(); $purchaseable = array(); $remaining = array(); foreach ( $product_types as $type ) { if ( ! isset( $product_data[ $type ]['slug'] ) ) { continue; } $purchaseable[] = $product_data[ $type ]; if ( ! in_array( $product_data[ $type ]['slug'], $installed, true ) ) { $remaining[] = $product_data[ $type ]['label']; } } return array( 'purchaseable' => $purchaseable, 'remaining' => $remaining, ); } } Onboarding/OnboardingJetpack.php 0000644 00000003464 15073235522 0012736 0 ustar 00 <?php /** * WooCommerce Onboarding Jetpack */ namespace Automattic\WooCommerce\Internal\Admin\Onboarding; /** * Contains logic around Jetpack setup during onboarding. */ class OnboardingJetpack { /** * Class instance. * * @var OnboardingJetpack instance */ private static $instance = null; /** * Get class instance. */ final public static function instance() { if ( ! static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init. */ public function init() { add_action( 'woocommerce_admin_plugins_pre_activate', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) ); add_action( 'woocommerce_admin_plugins_pre_install', array( $this, 'activate_and_install_jetpack_ahead_of_wcpay' ) ); // Always hook into Jetpack connection even if outside of admin. add_action( 'jetpack_site_registered', array( $this, 'set_woocommerce_setup_jetpack_opted_in' ) ); } /** * Sets the woocommerce_setup_jetpack_opted_in to true when Jetpack connects to WPCOM. */ public function set_woocommerce_setup_jetpack_opted_in() { update_option( 'woocommerce_setup_jetpack_opted_in', true ); } /** * Ensure that Jetpack gets installed and activated ahead of WooCommerce Payments * if both are being installed/activated at the same time. * * See: https://github.com/Automattic/woocommerce-payments/issues/1663 * See: https://github.com/Automattic/jetpack/issues/19624 * * @param array $plugins A list of plugins to install or activate. * * @return array */ public function activate_and_install_jetpack_ahead_of_wcpay( $plugins ) { if ( in_array( 'jetpack', $plugins, true ) && in_array( 'woocommerce-payments', $plugins, true ) ) { array_unshift( $plugins, 'jetpack' ); $plugins = array_unique( $plugins ); } return $plugins; } } Translations.php 0000644 00000027721 15073235522 0007753 0 ustar 00 <?php /** * Register the scripts, and handles items needed for managing translations within WooCommerce Admin. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Internal\Admin\Loader; /** * Translations Class. */ class Translations { /** * Class instance. * * @var Translations instance */ protected static $instance = null; /** * 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() { add_action( 'admin_enqueue_scripts', array( $this, 'potentially_load_translation_script_file' ), 15 ); // Combine JSON translation files (from chunks) when language packs are updated. add_action( 'upgrader_process_complete', array( $this, 'combine_translation_chunk_files' ), 10, 2 ); // Handler for WooCommerce and WooCommerce Admin plugin activation. add_action( 'woocommerce_activated_plugin', array( $this, 'potentially_generate_translation_strings' ) ); add_action( 'activated_plugin', array( $this, 'potentially_generate_translation_strings' ) ); } /** * Generate a filename to cache translations from JS chunks. * * @param string $domain Text domain. * @param string $locale Locale being retrieved. * @return string Filename. */ private function get_combined_translation_filename( $domain, $locale ) { $filename = implode( '-', array( $domain, $locale, WC_ADMIN_APP ) ) . '.json'; return $filename; } /** * Combines data from translation chunk files based on officially downloaded file format. * * @param array $json_i18n_filenames List of JSON chunk files. * @return array Combined translation chunk data. */ private function combine_official_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; } 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 <?php /** * WooCommerce Admin: Feature plugin main class. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones; use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes; use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn; use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments; use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PluginsHelper; use Automattic\WooCommerce\Admin\PluginsInstaller; use Automattic\WooCommerce\Admin\ReportExporter; use Automattic\WooCommerce\Admin\ReportsSync; use Automattic\WooCommerce\Internal\Admin\CategoryLookup; use Automattic\WooCommerce\Internal\Admin\Events; use Automattic\WooCommerce\Internal\Admin\Onboarding\Onboarding; /** * Feature plugin main class. * * @internal This file will not be bundled with woo core, only the feature plugin. * @internal Note this is not called WC_Admin due to a class already existing in core with that name. */ class FeaturePlugin { /** * The single instance of the class. * * @var object */ protected static $instance = null; /** * Constructor * * @return void */ protected function __construct() {} /** * Get class instance. * * @return object Instance. */ final public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Init the feature plugin, only if we can detect both Gutenberg and WooCommerce. */ public function init() { // Bail if WC isn't initialized (This can be called from WCAdmin's entrypoint). if ( ! defined( 'WC_ABSPATH' ) ) { return; } // Load the page controller functions file first to prevent fatal errors when disabling WooCommerce Admin. $this->define_constants(); require_once WC_ADMIN_ABSPATH . '/includes/react-admin/page-controller-functions.php'; require_once WC_ADMIN_ABSPATH . '/src/Admin/Notes/DeprecatedNotes.php'; require_once WC_ADMIN_ABSPATH . '/includes/react-admin/core-functions.php'; require_once WC_ADMIN_ABSPATH . '/includes/react-admin/feature-config.php'; require_once WC_ADMIN_ABSPATH . '/includes/react-admin/wc-admin-update-functions.php'; require_once WC_ADMIN_ABSPATH . '/includes/react-admin/class-experimental-abtest.php'; if ( did_action( 'plugins_loaded' ) ) { self::on_plugins_loaded(); } else { // Make sure we hook into `plugins_loaded` before core's Automattic\WooCommerce\Package::init(). // If core is network activated but we aren't, the packaged version of WooCommerce Admin will // attempt to use a data store that hasn't been loaded yet - because we've defined our constants here. // See: https://github.com/woocommerce/woocommerce-admin/issues/3869. add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), 9 ); } } /** * Setup plugin once all other plugins are loaded. * * @return void */ public function on_plugins_loaded() { $this->hooks(); $this->includes(); } /** * Define Constants. */ protected function define_constants() { $this->define( 'WC_ADMIN_APP', 'wc-admin-app' ); $this->define( 'WC_ADMIN_ABSPATH', WC_ABSPATH ); $this->define( 'WC_ADMIN_DIST_JS_FOLDER', 'assets/client/admin/' ); $this->define( 'WC_ADMIN_DIST_CSS_FOLDER', 'assets/client/admin/' ); $this->define( 'WC_ADMIN_PLUGIN_FILE', WC_PLUGIN_FILE ); /** * Define the WC Admin Images Folder URL. * * @deprecated 6.7.0 * @var string */ if ( ! defined( 'WC_ADMIN_IMAGES_FOLDER_URL' ) ) { /** * Define the WC Admin Images Folder URL. * * @deprecated 6.7.0 * @var string */ define( 'WC_ADMIN_IMAGES_FOLDER_URL', plugins_url( 'assets/images', WC_PLUGIN_FILE ) ); } /** * Define the current WC Admin version. * * @deprecated 6.4.0 * @var string */ if ( ! defined( 'WC_ADMIN_VERSION_NUMBER' ) ) { /** * Define the current WC Admin version. * * @deprecated 6.4.0 * @var string */ define( 'WC_ADMIN_VERSION_NUMBER', '3.3.0' ); } } /** * Include WC Admin classes. */ public function includes() { // Initialize Database updates, option migrations, and Notes. Events::instance()->init(); Notes::init(); // Initialize Plugins Installer. PluginsInstaller::init(); PluginsHelper::init(); // Initialize API. API\Init::instance(); if ( Features::is_enabled( 'onboarding' ) ) { Onboarding::init(); } if ( Features::is_enabled( 'analytics' ) ) { // Initialize Reports syncing. ReportsSync::init(); CategoryLookup::instance()->init(); // Initialize Reports exporter. ReportExporter::init(); } // Admin note providers. // @todo These should be bundled in the features/ folder, but loading them from there currently has a load order issue. new WooSubscriptionsNotes(); new OrderMilestones(); new TrackingOptIn(); new WooCommercePayments(); new InstallJPAndWCSPlugins(); new SellingOnlineCourses(); new MagentoMigration(); // Initialize MerchantEmailNotifications. MerchantEmailNotifications::init(); } /** * Set up our admin hooks and plugin loader. */ protected function hooks() { add_filter( 'woocommerce_admin_features', array( $this, 'replace_supported_features' ), 0 ); Loader::get_instance(); WCAdminAssets::get_instance(); } /** * Overwrites the allowed features array using a local `feature-config.php` file. * * @param array $features Array of feature slugs. */ public function replace_supported_features( $features ) { /** * Get additional feature config * * @since 6.5.0 */ $feature_config = apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() ); $features = array_keys( array_filter( $feature_config ) ); return $features; } /** * Define constant if not already set. * * @param string $name Constant name. * @param string|bool $value Constant value. */ protected function define( $name, $value ) { if ( ! defined( $name ) ) { define( $name, $value ); } } /** * Prevent cloning. */ private function __clone() {} /** * Prevent unserializing. */ public function __wakeup() { die(); } } Orders/COTRedirectionController.php 0000644 00000005405 15073235522 0013404 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; /** * When Custom Order Tables are not the default order store (ie, posts are authoritative), we should take care of * redirecting requests for the order editor and order admin list table to the equivalent posts-table screens. * * If the redirect logic is problematic, it can be unhooked using code like the following example: * * remove_action( * 'admin_page_access_denied', * array( wc_get_container()->get( COTRedirectionController::class ), 'handle_hpos_admin_requests' ) * ); */ class COTRedirectionController { use AccessiblePrivateMethods; /** * Add hooks needed to perform our magic. */ public function setup(): void { // Only take action in cases where access to the admin screen would otherwise be denied. self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) ); } /** * Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially * redirect the user to the equivalent CPT-driven screens. * * @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used. */ private function handle_hpos_admin_requests( $query_params = null ) { $query_params = is_array( $query_params ) ? $query_params : $_GET; if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) { return; } $params = wp_unslash( $query_params ); $action = $params['action'] ?? ''; unset( $params['page'] ); if ( 'edit' === $action && isset( $params['id'] ) ) { $params['post'] = $params['id']; unset( $params['id'] ); $new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) ); } elseif ( 'new' === $action ) { unset( $params['action'] ); $params['post_type'] = 'shop_order'; $new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) ); } else { // If nonce parameters are present and valid, rebuild them for the CPT admin list table. if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) { $params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' ); $params['_wpnonce'] = wp_create_nonce( 'bulk-posts' ); } // If an `id` array parameter is present, rename as `post`. if ( isset( $params['id'] ) && is_array( $params['id'] ) ) { $params['post'] = $params['id']; unset( $params['id'] ); } $params['post_type'] = 'shop_order'; $new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) ); } if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) { exit; } } } Orders/MetaBoxes/CustomMetaBox.php 0000644 00000036731 15073235522 0013152 0 ustar 00 <?php /** * Meta box to edit and add custom meta values for an order. */ namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes; use WC_Data_Store; use WC_Meta_Data; use WC_Order; use WP_Ajax_Response; /** * Class CustomMetaBox. */ class CustomMetaBox { /** * Update nonce shared among different meta rows. * * @var string */ private $update_nonce; /** * Helper method to get formatted meta data array with proper keys. This can be directly fed to `list_meta()` method. * * @param \WC_Order $order Order object. * * @return array Meta data. */ private function get_formatted_order_meta_data( \WC_Order $order ) { $metadata = $order->get_meta_data(); $metadata_to_list = array(); foreach ( $metadata as $meta ) { $data = $meta->get_data(); if ( is_protected_meta( $data['key'], 'order' ) ) { continue; } $metadata_to_list[] = array( 'meta_id' => $data['id'], 'meta_key' => $data['key'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- False positive, not a meta query. 'meta_value' => maybe_serialize( $data['value'] ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- False positive, not a meta query. ); } return $metadata_to_list; } /** * Renders the meta box to manage custom meta. * * @param \WP_Post|\WC_Order $order_or_post Post or order object that we are rendering for. */ public function output( $order_or_post ) { if ( is_a( $order_or_post, \WP_Post::class ) ) { $order = wc_get_order( $order_or_post ); } else { $order = $order_or_post; } $this->render_custom_meta_form( $this->get_formatted_order_meta_data( $order ), $order ); } /** * Helper method to render layout and actual HTML * * @param array $metadata_to_list List of metadata to render. * @param \WC_Order $order Order object. */ private function render_custom_meta_form( array $metadata_to_list, \WC_Order $order ) { ?> <div id="postcustomstuff"> <div id="ajax-response"></div> <?php list_meta( $metadata_to_list ); $this->render_meta_form( $order ); ?> </div> <p> <?php printf( /* translators: 1: opening documentation tag 2: closing documentation tag. */ esc_html( __( 'Custom fields can be used to add extra metadata to an order that you can %1$suse in your theme%2$s.', 'woocommerce' ) ), '<a href="' . esc_attr__( 'https://wordpress.org/support/article/custom-fields/', 'woocommerce' ) . '">', '</a>' ); ?> </p> <?php } /** * Compute keys to display in autofill when adding new meta key entry in custom meta box. * Currently, returns empty keys, will be implemented after caching is merged. * * @param array|null $keys Keys to display in autofill. * @param \WP_Post|\WC_Order $order Order object. * * @return array|mixed Array of keys to display in autofill. */ public function order_meta_keys_autofill( $keys, $order ) { if ( is_a( $order, \WC_Order::class ) ) { return array(); } return $keys; } /** * Reimplementation of WP core's `meta_form` function. Renders meta form box. * * @param \WC_Order $order WC_Order object. * * @return void */ public function render_meta_form( \WC_Order $order ) : void { $meta_key_input_id = 'metakeyselect'; $keys = $this->order_meta_keys_autofill( null, $order ); /** * 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 ); ?> <p><strong><?php esc_html_e( 'Add New Custom Field:', 'woocommerce' ); ?></strong></p> <table id="newmeta"> <thead> <tr> <th class="left"><label for="<?php echo esc_attr( $meta_key_input_id ); ?>"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label></th> <th><label for="metavalue"><?php esc_html_e( 'Value', 'woocommerce' ); ?></label></th> </tr> </thead> <tbody> <tr> <td id="newmetaleft" class="left"> <?php if ( $keys ) { ?> <select id="metakeyselect" name="metakeyselect"> <option value="#NONE#"><?php esc_html_e( '— Select —', 'woocommerce' ); ?></option> <?php foreach ( $keys as $key ) { if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_order', $order->get_id() ) ) { continue; } echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>'; } ?> </select> <input class="hide-if-js" type="text" id="metakeyinput" name="metakeyinput" value="" /> <a href="#postcustomstuff" class="hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggle();return false;"> <span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span> <span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span></a> <?php } else { ?> <input type="text" id="metakeyinput" name="metakeyinput" value="" /> <?php } ?> </td> <td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea></td> </tr> <tr><td colspan="2"> <div class="submit"> <?php submit_button( __( 'Add Custom Field', 'woocommerce' ), '', 'addmeta', false, array( 'id' => 'newmeta-submit', 'data-wp-lists' => 'add:the-list:newmeta', ) ); ?> </div> <?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?> </td></tr> </tbody> </table> <?php } /** * Helper method to verify order edit permissions. * * @param int $order_id Order ID. * * @return ?WC_Order WC_Order object if the user can edit the order, die otherwise. */ private function verify_order_edit_permission_for_ajax( int $order_id ): ?WC_Order { if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'edit_others_shop_orders' ) ) { wp_send_json_error( 'missing_capabilities' ); wp_die(); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_send_json_error( 'invalid_order_id' ); wp_die(); } return $order; } /** * Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables. */ public function add_meta_ajax() { if ( ! check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' ) ) { wp_send_json_error( 'invalid_nonce' ); wp_die(); } $order_id = (int) $_POST['order_id'] ?? 0; $order = $this->verify_order_edit_permission_for_ajax( $order_id ); if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) { wp_die( 1 ); } if ( isset( $_POST['metakeyinput'] ) ) { // add meta. $meta_key = sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ) ); $meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) ); $this->handle_add_meta( $order, $meta_key, $meta_value ); } else { // update. $meta = wp_unslash( $_POST['meta'] ?? array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk. $this->handle_update_meta( $order, $meta ); } } /** * Part of WP Core's `wp_ajax_add_meta`. This is re-implemented to support updating meta for custom tables. * * @param WC_Order $order Order object. * @param string $meta_key Meta key. * @param string $meta_value Meta value. * * @return void */ private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) { $count = 0; if ( is_protected_meta( $meta_key ) ) { wp_send_json_error( 'protected_meta' ); wp_die(); } $metas_for_current_key = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) ); $meta_ids = wp_list_pluck( $metas_for_current_key, 'id' ); $order->add_meta_data( $meta_key, $meta_value ); $order->save_meta_data(); $metas_for_current_key_with_new = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) ); $meta_id = 0; $new_meta_ids = wp_list_pluck( $metas_for_current_key_with_new, 'id' ); $new_meta_ids = array_values( array_diff( $new_meta_ids, $meta_ids ) ); if ( count( $new_meta_ids ) > 0 ) { $meta_id = $new_meta_ids[0]; } $response = new WP_Ajax_Response( array( 'what' => 'meta', 'id' => $meta_id, 'data' => $this->list_meta_row( array( 'meta_id' => $meta_id, 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query. 'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query. ), $count ), 'position' => 1, ) ); $response->send(); } /** * Handles updating metadata. * * @param WC_Order $order Order object. * @param array $meta Meta object to update. * * @return void */ private function handle_update_meta( WC_Order $order, array $meta ) { if ( ! is_array( $meta ) ) { wp_send_json_error( 'invalid_meta' ); wp_die(); } array_walk( $meta, 'sanitize_text_field' ); $mid = (int) key( $meta ); if ( ! $mid ) { wp_send_json_error( 'invalid_meta_id' ); wp_die(); } $key = $meta[ $mid ]['key']; $value = $meta[ $mid ]['value']; if ( is_protected_meta( $key ) ) { wp_send_json_error( 'protected_meta' ); wp_die(); } if ( '' === trim( $key ) ) { wp_send_json_error( 'invalid_meta_key' ); wp_die(); } $count = 0; $order->update_meta_data( $key, $value, $mid ); $order->save_meta_data(); $response = new WP_Ajax_Response( array( 'what' => 'meta', 'id' => $mid, 'old_id' => $mid, 'data' => $this->list_meta_row( array( 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query. 'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query. 'meta_id' => $mid, ), $count ), 'position' => 0, ) ); $response->send(); } /** * Outputs a single row of public meta data in the Custom Fields meta box. * * @since 2.5.0 * * @param array $entry Meta entry. * @param int $count Sequence number of meta entries. * @return string */ private function list_meta_row( array $entry, int &$count ) : string { if ( is_protected_meta( $entry['meta_key'], 'post' ) ) { return ''; } if ( ! $this->update_nonce ) { $this->update_nonce = wp_create_nonce( 'add-meta' ); } $r = ''; ++ $count; if ( is_serialized( $entry['meta_value'] ) ) { if ( is_serialized_string( $entry['meta_value'] ) ) { // This is a serialized string, so we should display it. $entry['meta_value'] = maybe_unserialize( $entry['meta_value'] ); // // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query. } else { // This is a serialized array/object so we should NOT display it. --$count; return ''; } } $entry['meta_key'] = esc_attr( $entry['meta_key'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query. $entry['meta_value'] = esc_textarea( $entry['meta_value'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query. $entry['meta_id'] = (int) $entry['meta_id']; $delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] ); $r .= "\n\t<tr id='meta-{$entry['meta_id']}'>"; $r .= "\n\t\t<td class='left'><label class='screen-reader-text' for='meta-{$entry['meta_id']}-key'>" . __( 'Key', 'woocommerce' ) . "</label><input name='meta[{$entry['meta_id']}][key]' id='meta-{$entry['meta_id']}-key' type='text' size='20' value='{$entry['meta_key']}' />"; $r .= "\n\t\t<div class='submit'>"; $r .= get_submit_button( __( 'Delete', 'woocommerce' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce:$delete_nonce" ) ); $r .= "\n\t\t"; $r .= get_submit_button( __( 'Update', 'woocommerce' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta={$this->update_nonce}" ) ); $r .= '</div>'; $r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false ); $r .= '</td>'; $r .= "\n\t\t<td><label class='screen-reader-text' for='meta-{$entry['meta_id']}-value'>" . __( 'Value', 'woocommerce' ) . "</label><textarea name='meta[{$entry['meta_id']}][value]' id='meta-{$entry['meta_id']}-value' rows='2' cols='30'>{$entry['meta_value']}</textarea></td>\n\t</tr>"; return $r; } /** * Reimplementation of WP core's `wp_ajax_delete_meta` method to support order custom meta updates with custom tables. * * @return void */ public function delete_meta_ajax() { $meta_id = (int) $_POST['id'] ?? 0; $order_id = (int) $_POST['order_id'] ?? 0; if ( ! $meta_id || ! $order_id ) { wp_send_json_error( 'invalid_meta_id' ); wp_die(); } check_ajax_referer( "delete-meta_$meta_id" ); $order = $this->verify_order_edit_permission_for_ajax( $order_id ); $meta_to_delete = wp_list_filter( $order->get_meta_data(), array( 'id' => $meta_id ) ); if ( empty( $meta_to_delete ) ) { wp_send_json_error( 'invalid_meta_id' ); wp_die(); } $order->delete_meta_data_by_mid( $meta_id ); if ( $order->save() ) { wp_die( 1 ); } wp_die( 0 ); } /** * Handle the possible changes in order metadata coming from an order edit page in admin * (labeled "custom fields" in the UI). * * This method expects the $_POST array to contain a 'meta' key that is an associative * array of [meta item id => [ 'key' => meta item name, 'value' => meta item value ]; * and also to contain (possibly empty) 'metakeyinput' and 'metavalue' keys. * * @param WC_Order $order The order to handle. */ public function handle_metadata_changes( $order ) { $has_meta_changes = false; $order_meta = $order->get_meta_data(); $order_meta = array_combine( array_map( fn( $meta ) => $meta->id, $order_meta ), $order_meta ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing foreach ( ( $_POST['meta'] ?? array() ) as $request_meta_id => $request_meta_data ) { $request_meta_id = wp_unslash( $request_meta_id ); $request_meta_key = wp_unslash( $request_meta_data['key'] ); $request_meta_value = wp_unslash( $request_meta_data['value'] ); if ( array_key_exists( $request_meta_id, $order_meta ) && ( $order_meta[ $request_meta_id ]->key !== $request_meta_key || $order_meta[ $request_meta_id ]->value !== $request_meta_value ) ) { $order->update_meta_data( $request_meta_key, $request_meta_value, $request_meta_id ); $has_meta_changes = true; } } $request_new_key = wp_unslash( $_POST['metakeyinput'] ?? '' ); $request_new_value = wp_unslash( $_POST['metavalue'] ?? '' ); if ( '' !== $request_new_key ) { $order->add_meta_data( $request_new_key, $request_new_value ); $has_meta_changes = true; } // phpcs:enable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing if ( $has_meta_changes ) { $order->save(); } } } Orders/MetaBoxes/CustomerHistory.php 0000644 00000002433 15073235522 0013573 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes; use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta; use WC_Order; /** * Class CustomerHistory * * @since 8.5.0 */ class CustomerHistory { use OrderAttributionMeta; /** * Output the customer history template for the order. * * @param WC_Order $order The order object. * * @return void */ public function output( WC_Order $order ): void { $this->display_customer_history( $order->get_customer_id(), $order->get_billing_email() ); } /** * Display the customer history template for the customer. * * @param int $customer_id The customer ID. * @param string $billing_email The customer billing email. * * @return void */ private function display_customer_history( int $customer_id, string $billing_email ): void { $has_customer_id = false; if ( $customer_id ) { $has_customer_id = true; $args = $this->get_customer_history( $customer_id ); } elseif ( $billing_email ) { $args = $this->get_customer_history( $billing_email ); } else { $args = array( 'order_count' => 0, 'total_spent' => 0, 'average_spent' => 0, ); } $args['has_customer_id'] = $has_customer_id; wc_get_template( 'order/customer-history.php', $args ); } } Orders/MetaBoxes/TaxonomiesMetaBox.php 0000644 00000010430 15073235522 0014012 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; /** * TaxonomiesMetaBox class, renders taxonomy sidebar widget on order edit screen. */ class TaxonomiesMetaBox { /** * Order Table data store class. * * @var OrdersTableDataStore */ private $orders_table_data_store; /** * Dependency injection init method. * * @param OrdersTableDataStore $orders_table_data_store Order Table data store class. * * @return void */ public function init( OrdersTableDataStore $orders_table_data_store ) { $this->orders_table_data_store = $orders_table_data_store; } /** * Registers meta boxes to be rendered in order edit screen for taxonomies. * * Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it. * * @param string $screen_id Screen ID. * @param string $order_type Order type to register meta boxes for. * * @return void */ public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) { include_once ABSPATH . 'wp-admin/includes/meta-boxes.php'; $taxonomies = get_object_taxonomies( $order_type ); // All taxonomies. foreach ( $taxonomies as $tax_name ) { $taxonomy = get_taxonomy( $tax_name ); if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) { continue; } if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) { $taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' ); } if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) { $taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' ); } $label = $taxonomy->labels->name; if ( ! is_taxonomy_hierarchical( $tax_name ) ) { $tax_meta_box_id = 'tagsdiv-' . $tax_name; } else { $tax_meta_box_id = $tax_name . 'div'; } add_meta_box( $tax_meta_box_id, $label, $taxonomy->meta_box_cb, $screen_id, 'side', 'core', array( 'taxonomy' => $tax_name, '__back_compat_meta_box' => true, ) ); } } /** * Save handler for taxonomy data. * * @param \WC_Abstract_Order $order Order object. * @param array|null $taxonomy_input Taxonomy input passed from input. */ public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) { if ( ! isset( $taxonomy_input ) ) { return; } $sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input ); $sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input ); $this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input ); } /** * Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy. * * @param array|null $taxonomy_data Nonce verified taxonomy input. * * @return array Sanitized taxonomy input. */ private function sanitize_tax_input( $taxonomy_data ) : array { $sanitized_tax_input = array(); if ( ! is_array( $taxonomy_data ) ) { return $sanitized_tax_input; } // Convert taxonomy input to term IDs, to avoid ambiguity. foreach ( $taxonomy_data as $taxonomy => $terms ) { $tax_object = get_taxonomy( $taxonomy ); if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) { $sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) ); } } return $sanitized_tax_input; } /** * Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box. * * @param \WC_Abstract_Order $order Order object. * @param array $box Meta box args. * * @return void */ public function order_categories_meta_box( $order, $box ) { $post = get_post( $order->get_id() ); post_categories_meta_box( $post, $box ); } /** * Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box. * * @param \WC_Abstract_Order $order Order object. * @param array $box Meta box args. * * @return void */ public function order_tags_meta_box( $order, $box ) { $post = get_post( $order->get_id() ); post_tags_meta_box( $post, $box ); } } Orders/MetaBoxes/OrderAttribution.php 0000644 00000003424 15073235522 0013711 0 ustar 00 <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes; use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta; use WC_Order; /** * Class OrderAttribution * * @since 8.5.0 */ class OrderAttribution { use OrderAttributionMeta; /** * OrderAttribution constructor. */ public function __construct() { $this->set_fields_and_prefix(); } /** * Format the meta data for display. * * @since 8.5.0 * * @param array $meta The array of meta data to format. * * @return void */ public function format_meta_data( array &$meta ) { if ( array_key_exists( 'device_type', $meta ) ) { switch ( $meta['device_type'] ) { case 'Mobile': $meta['device_type'] = __( 'Mobile', 'woocommerce' ); break; case 'Tablet': $meta['device_type'] = __( 'Tablet', 'woocommerce' ); break; case 'Desktop': $meta['device_type'] = __( 'Desktop', 'woocommerce' ); break; default: $meta['device_type'] = __( 'Unknown', 'woocommerce' ); break; } } } /** * Output the attribution data metabox for the order. * * @since 8.5.0 * * @param WC_Order $order The order object. * * @return void */ public function output( WC_Order $order ) { $meta = $this->filter_meta_data( $order->get_meta_data() ); // If we don't have any meta to show, return. if ( empty( $meta ) ) { esc_html_e( 'No order source data available.', 'woocommerce' ); return; } $this->format_meta_data( $meta ); $template_data = array( 'meta' => $meta, // Only show more details toggle if there is more than just the origin. 'has_more_details' => array( 'origin' ) !== array_keys( $meta ), ); wc_get_template( 'order/attribution-data-fields.php', $template_data ); } } Orders/ListTable.php 0000644 00000136451 15073235522 0010414 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\Utilities\OrderUtil; use WC_Order; use WP_List_Table; use WP_Screen; /** * Admin list table for orders as managed by the OrdersTableDataStore. */ class ListTable extends WP_List_Table { /** * Order type. * * @var string */ private $order_type; /** * Request vars. * * @var array */ private $request = array(); /** * Contains the arguments to be used in the order query. * * @var array */ private $order_query_args = array(); /** * Tracks if a filter (ie, date or customer filter) has been applied. * * @var bool */ private $has_filter = false; /** * Page controller instance for this request. * * @var PageController */ private $page_controller; /** * Tracks whether we're currently inside the trash. * * @var boolean */ private $is_trash = false; /** * Caches order counts by status. * * @var array */ private $status_count_cache = null; /** * Sets up the admin list table for orders (specifically, for orders managed by the OrdersTableDataStore). * * @see WC_Admin_List_Table_Orders for the corresponding class used in relation to the traditional WP Post store. */ public function __construct() { parent::__construct( array( 'singular' => 'order', 'plural' => 'orders', 'ajax' => false, ) ); } /** * Init method, invoked by DI container. * * @internal This method is not intended to be used directly (except for testing). * @param PageController $page_controller Page controller instance for this request. */ final public function init( PageController $page_controller ) { $this->page_controller = $page_controller; } /** * Performs setup work required before rendering the table. * * @param array $args Args to initialize this list table. * * @return void */ public function setup( $args = array() ): void { $this->order_type = $args['order_type'] ?? 'shop_order'; add_action( 'admin_notices', array( $this, 'bulk_action_notices' ) ); add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 ); add_filter( 'set_screen_option_edit_' . $this->order_type . '_per_page', array( $this, 'set_items_per_page' ), 10, 3 ); add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 ); add_action( 'admin_footer', array( $this, 'enqueue_scripts' ) ); add_action( 'woocommerce_order_list_table_restrict_manage_orders', array( $this, 'customers_filter' ) ); $this->items_per_page(); set_screen_options(); add_action( 'manage_' . wc_get_page_screen_id( $this->order_type ) . '_custom_column', array( $this, 'render_column' ), 10, 2 ); } /** * Generates content for a single row of the table. * * @since 7.8.0 * * @param \WC_Order $order The current order. */ public function single_row( $order ) { /** * Filters the list of CSS class names for a given order row in the orders list table. * * @since 7.8.0 * * @param string[] $classes An array of CSS class names. * @param \WC_Order $order The order object. */ $css_classes = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_order_css_classes', array( 'order-' . $order->get_id(), 'type-' . $order->get_type(), 'status-' . $order->get_status(), ), $order ); $css_classes = array_unique( array_map( 'trim', $css_classes ) ); // Is locked? $edit_lock = wc_get_container()->get( EditLock::class ); if ( $edit_lock->is_locked_by_another_user( $order ) ) { $css_classes[] = 'wp-locked'; } echo '<tr id="order-' . esc_attr( $order->get_id() ) . '" class="' . esc_attr( implode( ' ', $css_classes ) ) . '">'; $this->single_row_columns( $order ); echo '</tr>'; } /** * Render individual column. * * @param string $column_id Column ID to render. * @param WC_Order $order Order object. */ public function render_column( $column_id, $order ) { if ( ! $order ) { return; } if ( is_callable( array( $this, 'render_' . $column_id . '_column' ) ) ) { call_user_func( array( $this, 'render_' . $column_id . '_column' ), $order ); } } /** * Handles output for the default column. * * @param \WC_Order $order Current WooCommerce order object. * @param string $column_name Identifier for the custom column. */ public function column_default( $order, $column_name ) { /** * Fires for each custom column for a specific order type. This hook takes precedence over the generic * action `manage_{$this->screen->id}_custom_column`. * * @param string $column_name Identifier for the custom column. * @param \WC_Order $order Current WooCommerce order object. * * @since 7.3.0 */ do_action( 'woocommerce_' . $this->order_type . '_list_table_custom_column', $column_name, $order ); /** * Fires for each custom column in the Custom Order Table in the administrative screen. * * @param string $column_name Identifier for the custom column. * @param \WC_Order $order Current WooCommerce order object. * * @since 7.0.0 */ do_action( "manage_{$this->screen->id}_custom_column", $column_name, $order ); } /** * Sets up an items-per-page control. */ private function items_per_page(): void { add_screen_option( 'per_page', array( 'default' => 20, 'option' => 'edit_' . $this->order_type . '_per_page', ) ); } /** * Saves the items-per-page setting. * * @param mixed $default The default value. * @param string $option The option being configured. * @param int $value The submitted option value. * * @return mixed */ public function set_items_per_page( $default, string $option, int $value ) { return 'edit_' . $this->order_type . '_per_page' === $option ? absint( $value ) : $default; } /** * Render the table. * * @return void */ public function display() { $post_type = get_post_type_object( $this->order_type ); $title = esc_html( $post_type->labels->name ); $add_new = esc_html( $post_type->labels->add_new ); $new_page_link = $this->page_controller->get_new_page_url( $this->order_type ); $search_label = ''; if ( ! empty( $this->order_query_args['s'] ) ) { $search_label = '<span class="subtitle">'; $search_label .= sprintf( /* translators: %s: Search query. */ __( 'Search results for: %s', 'woocommerce' ), '<strong>' . esc_html( $this->order_query_args['s'] ) . '</strong>' ); $search_label .= '</span>'; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wp_kses_post( " <div class='wrap'> <h1 class='wp-heading-inline'>{$title}</h1> <a href='" . esc_url( $new_page_link ) . "' class='page-title-action'>{$add_new}</a> {$search_label} <hr class='wp-header-end'>" ); if ( $this->should_render_blank_state() ) { $this->render_blank_state(); return; } $this->views(); echo '<form id="wc-orders-filter" method="get" action="' . esc_url( get_admin_url( null, 'admin.php' ) ) . '">'; $this->print_hidden_form_fields(); $this->search_box( esc_html__( 'Search orders', 'woocommerce' ), 'orders-search-input' ); parent::display(); echo '</form> </div>'; } /** * Renders advice in the event that no orders exist yet. * * @return void */ public function render_blank_state(): void { ?> <div class="woocommerce-BlankState"> <h2 class="woocommerce-BlankState-message"> <?php esc_html_e( 'When you receive a new order, it will appear here.', 'woocommerce' ); ?> </h2> <div class="woocommerce-BlankState-buttons"> <a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://woo.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin"><?php esc_html_e( 'Learn more about orders', 'woocommerce' ); ?></a> </div> <?php /** * Renders after the 'blank state' message for the order list table has rendered. * * @since 6.6.1 */ do_action( 'wc_marketplace_suggestions_orders_empty_state' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment ?> </div> <?php } /** * Retrieves the list of bulk actions available for this table. * * @return array */ protected function get_bulk_actions() { $selected_status = $this->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 "<a href='$url' $class>$name <span class='count'>($count)</span></a>"; } /** * Extra controls to be displayed between bulk actions and pagination. * * @param string $which Either 'top' or 'bottom'. */ protected function extra_tablenav( $which ) { echo '<div class="alignleft actions">'; 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 '</div>'; } /** * 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 '<select name="m" id="filter-by-date">'; echo '<option ' . selected( $m, 0, false ) . ' value="0">' . esc_html__( 'All dates', 'woocommerce' ) . '</option>'; foreach ( $order_dates as $date ) { $month = zeroise( $date->month, 2 ); $month_year_text = sprintf( /* translators: 1: Month name, 2: 4-digit year. */ esc_html_x( '%1$s %2$d', 'order dates dropdown', 'woocommerce' ), $wp_locale->get_month( $month ), $date->year ); printf( '<option %1$s value="%2$s">%3$s</option>\n', selected( $m, $date->year . $month, false ), esc_attr( $date->year . $month ), esc_html( $month_year_text ) ); } echo '</select>'; } /** * 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. ?> <select class="wc-customer-search" name="_customer_user" data-placeholder="<?php esc_attr_e( 'Filter by registered customer', 'woocommerce' ); ?>" data-allow_clear="true"> <option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></option> </select> <?php } /** * Get list columns. * * @return array */ public function get_columns() { /** * Filters the list of columns. * * @param array $columns List of sortable columns. * * @since 7.3.0 */ return apply_filters( 'woocommerce_' . $this->order_type . '_list_table_columns', array( 'cb' => '<input type="checkbox" />', '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(); ?> <input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="id[]" value="<?php echo esc_attr( $item->get_id() ); ?>" /> <div class="locked-indicator"> <span class="locked-indicator-icon" aria-hidden="true"></span> <span class="screen-reader-text"> <?php // translators: %s is an order ID. echo esc_html( sprintf( __( 'Order %s is locked.', 'woocommerce' ), $item->get_id() ) ); ?> </span> </div> <?php return ob_get_clean(); } /** * Renders the order number, customer name and provides a preview link. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_order_number_column( WC_Order $order ): void { $buyer = ''; if ( $order->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 '<strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong>'; } else { echo '<a href="#" class="order-preview" data-order-id="' . absint( $order->get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '</a>'; echo '<a href="' . esc_url( $this->get_order_edit_link( $order ) ) . '" class="order-view"><strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>'; } } /** * 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( '<time datetime="%1$s" title="%2$s">%3$s</time>', 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 . '<br/><small style="display:block">' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '</small>' ); } 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( '<mark class="order-status %s tips" data-tip="%s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), wp_kses_post( $tooltip ), esc_html( $status_name ) ); } else { printf( '<mark class="order-status %s"><span>%s</span></mark>', 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( '#<br\s*/?>#i', ', ', $address ) ); if ( $order->get_payment_method() ) { /* translators: %s: payment method */ echo '<span class="description">' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_payment_method_title() ) ) . '</span>'; } } 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 '<a target="_blank" href="' . esc_url( $order->get_shipping_address_map_url() ) . '">' . esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) ) . '</a>'; if ( $order->get_shipping_method() ) { /* translators: %s: shipping method */ echo '<span class="description">' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_shipping_method() ) ) . '</span>'; } } 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 '<span class="tips" data-tip="' . esc_attr( sprintf( __( 'via %s', 'woocommerce' ), $order->get_payment_method_title() ) ) . '">' . wp_kses_post( $order->get_formatted_order_total() ) . '</span>'; } 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 '<p>'; /** * 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 '</p>'; } /** * Outputs hidden fields used to retain state when filtering. * * @return void */ private function print_hidden_form_fields(): void { echo '<input type="hidden" name="page" value="wc-orders' . ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type ) . '" >'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $state_params = array( 'paged', 'status', ); foreach ( $state_params as $param ) { if ( ! isset( $_GET[ $param ] ) ) { continue; } echo '<input type="hidden" name="' . esc_attr( $param ) . '" value="' . esc_attr( sanitize_text_field( wp_unslash( $_GET[ $param ] ) ) ) . '" >'; } } /** * 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 <status>" 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 '<div class="updated"><p>' . esc_html( $message ) . '</p></div>'; 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 '<div class="updated"><p>' . esc_html( $message ) . '</p></div>'; } } /** * 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(); ?> <script type="text/template" id="tmpl-wc-modal-view-order"> <div class="wc-backbone-modal wc-order-preview"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <mark class="order-status status-{{ data.status }}"><span>{{ data.status_name }}</span></mark> <?php /* translators: %s: order ID */ ?> <h1><?php echo esc_html( sprintf( __( 'Order #%s', 'woocommerce' ), '{{ data.order_number }}' ) ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span> </button> </header> <article> <?php do_action( 'woocommerce_admin_order_preview_start' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?> <div class="wc-order-preview-addresses"> <div class="wc-order-preview-address"> <h2><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h2> {{{ data.formatted_billing_address }}} <# if ( data.data.billing.email ) { #> <strong><?php esc_html_e( 'Email', 'woocommerce' ); ?></strong> <a href="mailto:{{ data.data.billing.email }}">{{ data.data.billing.email }}</a> <# } #> <# if ( data.data.billing.phone ) { #> <strong><?php esc_html_e( 'Phone', 'woocommerce' ); ?></strong> <a href="tel:{{ data.data.billing.phone }}">{{ data.data.billing.phone }}</a> <# } #> <# if ( data.payment_via ) { #> <strong><?php esc_html_e( 'Payment via', 'woocommerce' ); ?></strong> {{{ data.payment_via }}} <# } #> </div> <# if ( data.needs_shipping ) { #> <div class="wc-order-preview-address"> <h2><?php esc_html_e( 'Shipping details', 'woocommerce' ); ?></h2> <# if ( data.ship_to_billing ) { #> {{{ data.formatted_billing_address }}} <# } else { #> <a href="{{ data.shipping_address_map_url }}" target="_blank">{{{ data.formatted_shipping_address }}}</a> <# } #> <# if ( data.shipping_via ) { #> <strong><?php esc_html_e( 'Shipping method', 'woocommerce' ); ?></strong> {{ data.shipping_via }} <# } #> </div> <# } #> <# if ( data.data.customer_note ) { #> <div class="wc-order-preview-note"> <strong><?php esc_html_e( 'Note', 'woocommerce' ); ?></strong> {{ data.data.customer_note }} </div> <# } #> </div> {{{ data.item_html }}} <?php do_action( 'woocommerce_admin_order_preview_end' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?> </article> <footer> <div class="inner"> {{{ data.actions_html }}} <a class="button button-primary button-large" aria-label="<?php esc_attr_e( 'Edit this order', 'woocommerce' ); ?>" href="<?php echo $order_edit_url_placeholder; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <?php $html = ob_get_clean(); return $html; } } Orders/Edit.php 0000644 00000035652 15073235522 0007417 0 ustar 00 <?php /** * Renders order edit page, works with both post and order object. */ namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\OrderAttribution; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox; use Automattic\WooCommerce\Internal\Features\FeaturesController; use WC_Order; /** * Class Edit. */ class Edit { /** * Screen ID for the edit order screen. * * @var string */ private $screen_id; /** * Instance of the CustomMetaBox class. Used to render meta box for custom meta. * * @var CustomMetaBox */ private $custom_meta_box; /** * Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies. * * @var TaxonomiesMetaBox */ private $taxonomies_meta_box; /** * Instance of WC_Order to be used in metaboxes. * * @var \WC_Order */ private $order; /** * Action name that the form is currently handling. Could be new_order or edit_order. * * @var string */ private $current_action; /** * Message to be displayed to the user. Index of message from the messages array registered when declaring shop_order post type. * * @var int */ private $message; /** * Controller for orders page. Used to determine redirection URLs. * * @var PageController */ private $orders_page_controller; /** * Hooks all meta-boxes for order edit page. This is static since this may be called by post edit form rendering. * * @param string $screen_id Screen ID. * @param string $title Title of the page. */ public static function add_order_meta_boxes( string $screen_id, string $title ) { /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Data::output', $screen_id, 'normal', 'high' ); add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $screen_id, 'normal', 'high' ); /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Notes::output', $screen_id, 'side', 'default' ); add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $screen_id, 'normal', 'default' ); /* Translators: %s order type name. */ add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Actions::output', $screen_id, 'side', 'high' ); self::maybe_register_order_attribution( $screen_id, $title ); } /** * Hooks metabox save functions for order edit page. * * @return void */ public static function add_save_meta_boxes() { /** * Save Order Meta Boxes. * * In order: * Save the order items. * Save the order totals. * Save the order downloads. * Save order data - also updates status and sends out admin emails if needed. Last to show latest data. * Save actions - sends out other emails. Last to show latest data. */ add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 ); } /** * Enqueue necessary scripts for order edit page. */ private function enqueue_scripts() { if ( wp_is_mobile() ) { wp_enqueue_script( 'jquery-touch-punch' ); } wp_enqueue_script( 'post' ); // Ensure existing JS libraries are still available for backward compat. } /** * Returns the PageController for this edit form. This method is protected to allow child classes to overwrite the PageController object and return custom links. * * @since 8.0.0 * * @return PageController PageController object. */ protected function get_page_controller() { if ( ! isset( $this->orders_page_controller ) ) { $this->orders_page_controller = wc_get_container()->get( PageController::class ); } return $this->orders_page_controller; } /** * Setup hooks, actions and variables needed to render order edit page. * * @param \WC_Order $order Order object. */ public function setup( \WC_Order $order ) { $this->order = $order; $current_screen = get_current_screen(); $current_screen->is_block_editor( false ); $this->screen_id = $current_screen->id; if ( ! isset( $this->custom_meta_box ) ) { $this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class ); } if ( ! isset( $this->taxonomies_meta_box ) ) { $this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class ); } $this->add_save_meta_boxes(); $this->handle_order_update(); $this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) ); $this->add_order_specific_meta_box(); $this->add_order_taxonomies_meta_box(); /** * From wp-admin/includes/meta-boxes.php. * * Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here. * * @since 3.8.0. */ do_action( 'add_meta_boxes', $this->screen_id, $this->order ); /** * Provides an opportunity to inject custom meta boxes into the order editor screen. This * hook is an analog of `add_meta_boxes_<POST_TYPE>` as provided by WordPress core. * * @since 7.4.0 * * @oaram WC_Order $order The order being edited. */ do_action( 'add_meta_boxes_' . $this->screen_id, $this->order ); $this->enqueue_scripts(); } /** * Set the current action for the form. * * @param string $action Action name. */ public function set_current_action( string $action ) { $this->current_action = $action; } /** * Hooks meta box for order specific meta. */ private function add_order_specific_meta_box() { add_meta_box( 'order_custom', __( 'Custom Fields', 'woocommerce' ), array( $this, 'render_custom_meta_box' ), $this->screen_id, 'normal' ); } /** * Render custom meta box. * * @return void */ private function add_order_taxonomies_meta_box() { $this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() ); } /** * Register order attribution meta boxes if the feature is enabled. * * @since 8.5.0 * * @param string $screen_id Screen ID. * @param string $title Title of the page. * * @return void */ private static function maybe_register_order_attribution( string $screen_id, string $title ) { /** * Features controller. * * @var FeaturesController $feature_controller */ $feature_controller = wc_get_container()->get( FeaturesController::class ); if ( ! $feature_controller->feature_is_enabled( 'order_attribution' ) ) { return; } /** * Order attribution meta box. * * @var OrderAttribution $order_attribution_meta_box */ $order_attribution_meta_box = wc_get_container()->get( OrderAttribution::class ); add_meta_box( 'woocommerce-order-source-data', /* Translators: %s order type name. */ sprintf( __( '%s attribution', 'woocommerce' ), $title ), function( $post_or_order ) use ( $order_attribution_meta_box ) { $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order ); if ( $order instanceof WC_Order ) { $order_attribution_meta_box->output( $order ); } }, $screen_id, 'side', 'high' ); /** * Customer history meta box. * * @var CustomerHistory $customer_history_meta_box */ $customer_history_meta_box = wc_get_container()->get( CustomerHistory::class ); add_meta_box( 'woocommerce-customer-history', __( 'Customer history', 'woocommerce' ), function( $post_or_order ) use ( $customer_history_meta_box ) { $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order ); if ( $order instanceof WC_Order ) { $customer_history_meta_box->output( $order ); } }, $screen_id, 'side', 'high' ); } /** * Takes care of updating order data. Fires action that metaboxes can hook to for order data updating. * * @return void */ public function handle_order_update() { if ( ! isset( $this->order ) ) { return; } if ( 'edit_order' !== sanitize_text_field( wp_unslash( $_POST['action'] ?? '' ) ) ) { return; } check_admin_referer( $this->get_order_edit_nonce_action() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object. $taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null; $this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input ); /** * Save meta for shop order. * * @param int Order ID. * @param \WC_Order Post object. * * @since 2.1.0 */ do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order ); $this->custom_meta_box->handle_metadata_changes($this->order); // Order updated message. $this->message = 1; // Claim lock. $edit_lock = wc_get_container()->get( EditLock::class ); $edit_lock->lock( $this->order ); $this->redirect_order( $this->order ); } /** * Helper method to redirect to order edit page. * * @since 8.0.0 * * @param \WC_Order $order Order object. */ private function redirect_order( \WC_Order $order ) { $redirect_to = $this->get_page_controller()->get_edit_url( $order->get_id() ); if ( isset( $this->message ) ) { $redirect_to = add_query_arg( 'message', $this->message, $redirect_to ); } wp_safe_redirect( /** * Filter the URL used to redirect after an order is updated. Similar to the WP post's `redirect_post_location` filter. * * @param string $redirect_to The redirect destination URL. * @param int $order_id The order ID. * @param \WC_Order $order The order object. * * @since 8.0.0 */ apply_filters( 'woocommerce_redirect_order_location', $redirect_to, $order->get_id(), $order ) ); exit; } /** * Helper method to get the name of order edit nonce. * * @return string Nonce action name. */ private function get_order_edit_nonce_action() { return 'update-order_' . $this->order->get_id(); } /** * Render meta box for order specific meta. */ public function render_custom_meta_box() { $this->custom_meta_box->output( $this->order ); } /** * Render order edit page. */ public function display() { /** * This is used by the order edit page to show messages in the notice fields. * It should be similar to post_updated_messages filter, i.e.: * array( * {order_type} => array( * 1 => 'Order updated.', * 2 => 'Custom field updated.', * ... * ). * * The index to be displayed is computed from the $_GET['message'] variable. * * @since 7.4.0. */ $messages = apply_filters( 'woocommerce_order_updated_messages', array() ); $message = $this->message; if ( isset( $_GET['message'] ) ) { $message = absint( $_GET['message'] ); } if ( isset( $message ) ) { $message = $messages[ $this->order->get_type() ][ $message ] ?? false; } $this->render_wrapper_start( '', $message ); $this->render_meta_boxes(); $this->render_wrapper_end(); } /** * Helper function to render wrapper start. * * @param string $notice Notice to display, if any. * @param string $message Message to display, if any. */ private function render_wrapper_start( $notice = '', $message = '' ) { $post_type = get_post_type_object( $this->order->get_type() ); $edit_page_url = $this->get_page_controller()->get_edit_url( $this->order->get_id() ); $form_action = 'edit_order'; $referer = wp_get_referer(); $new_page_url = $this->get_page_controller()->get_new_page_url( $this->order->get_type() ); ?> <div class="wrap"> <h1 class="wp-heading-inline"> <?php echo 'new_order' === $this->current_action ? esc_html( $post_type->labels->add_new_item ) : esc_html( $post_type->labels->edit_item ); ?> </h1> <?php if ( 'edit_order' === $this->current_action ) { echo ' <a href="' . esc_url( $new_page_url ) . '" class="page-title-action">' . esc_html( $post_type->labels->add_new ) . '</a>'; } ?> <hr class="wp-header-end"> <?php if ( $notice ) : ?> <div id="notice" class="notice notice-warning"><p id="has-newer-autosave"><?php echo wp_kses_post( $notice ); ?></p></div> <?php endif; ?> <?php if ( $message ) : ?> <div id="message" class="updated notice notice-success is-dismissible"> <p><?php echo wp_kses_post( $message ); ?></p></div> <?php endif; ?> <form name="order" action="<?php echo esc_url( $edit_page_url ); ?>" method="post" id="order" <?php /** * Fires inside the order edit form tag. * * @param \WC_Order $order Order object. * * @since 6.9.0 */ do_action( 'order_edit_form_tag', $this->order ); ?> > <?php wp_nonce_field( $this->get_order_edit_nonce_action() ); ?> <?php /** * Fires at the top of the order edit form. Can be used as a replacement for edit_form_top hook for HPOS. * * @param \WC_Order $order Order object. * * @since 8.0.0 */ do_action( 'order_edit_form_top', $this->order ); wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?> <input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/> <input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/> <input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/> <input type="hidden" id="post_ID" name="post_ID" value="<?php echo esc_attr( $this->order->get_id() ); ?>"/> <div id="poststuff"> <div id="post-body" class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>"> <?php } /** * Helper function to render meta boxes. */ private function render_meta_boxes() { ?> <div id="postbox-container-1" class="postbox-container"> <?php do_meta_boxes( $this->screen_id, 'side', $this->order ); ?> </div> <div id="postbox-container-2" class="postbox-container"> <?php do_meta_boxes( $this->screen_id, 'normal', $this->order ); do_meta_boxes( $this->screen_id, 'advanced', $this->order ); ?> </div> <?php } /** * Helper function to render wrapper end. */ private function render_wrapper_end() { ?> </div> <!-- /post-body --> </div> <!-- /poststuff --> </form> </div> <!-- /wrap --> <?php } } Orders/PostsRedirectionController.php 0000644 00000011537 15073235522 0014072 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; /** * When {@see OrdersTableDataStore} is in use, this class takes care of redirecting admins from CPT-based URLs * to the new ones. */ class PostsRedirectionController { /** * Instance of the PageController class. * * @var PageController */ private $page_controller; /** * Constructor. * * @param PageController $page_controller Page controller instance. Used to generate links/URLs. */ public function __construct( PageController $page_controller ) { $this->page_controller = $page_controller; if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { return; } add_action( 'load-edit.php', function() { $this->maybe_redirect_to_orders_page(); } ); add_action( 'load-post-new.php', function() { $this->maybe_redirect_to_new_order_page(); } ); add_action( 'load-post.php', function() { $this->maybe_redirect_to_edit_order_page(); } ); } /** * If needed, performs a redirection to the main orders page. * * @return void */ private function maybe_redirect_to_orders_page(): void { $post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) { return; } // Respect query args, except for 'post_type'. $query_args = wp_unslash( $_GET ); $action = $query_args['action'] ?? ''; $posts = $query_args['post'] ?? array(); unset( $query_args['post_type'], $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] ); // Remap 'post_status' arg. if ( isset( $query_args['post_status'] ) ) { $query_args['status'] = $query_args['post_status']; unset( $query_args['post_status'] ); } $new_url = $this->page_controller->get_base_page_url( $post_type ); $new_url = add_query_arg( $query_args, $new_url ); // Handle bulk actions. if ( $action && in_array( $action, array( 'trash', 'untrash', 'delete', 'mark_processing', 'mark_on-hold', 'mark_completed', 'mark_cancelled' ), true ) ) { check_admin_referer( 'bulk-posts' ); $new_url = add_query_arg( array( 'action' => $action, 'id' => $posts, '_wp_http_referer' => $this->page_controller->get_orders_url(), '_wpnonce' => wp_create_nonce( 'bulk-orders' ), ), $new_url ); } wp_safe_redirect( $new_url, 301 ); exit; } /** * If needed, performs a redirection to the new order page. * * @return void */ private function maybe_redirect_to_new_order_page(): void { $post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) { return; } // Respect query args, except for 'post_type'. $query_args = wp_unslash( $_GET ); unset( $query_args['post_type'] ); $new_url = $this->page_controller->get_new_page_url( $post_type ); $new_url = add_query_arg( $query_args, $new_url ); wp_safe_redirect( $new_url, 301 ); exit; } /** * If needed, performs a redirection to the edit order page. * * @return void */ private function maybe_redirect_to_edit_order_page(): void { $post_id = absint( $_GET['post'] ?? 0 ); $redirect_from_types = wc_get_order_types( 'admin-menu' ); $redirect_from_types[] = 'shop_order_placehold'; if ( ! $post_id || ! in_array( get_post_type( $post_id ), $redirect_from_types, true ) || ! isset( $_GET['action'] ) ) { return; } // Respect query args, except for 'post'. $query_args = wp_unslash( $_GET ); $action = $query_args['action']; unset( $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] ); $new_url = ''; switch ( $action ) { case 'edit': $new_url = $this->page_controller->get_edit_url( $post_id ); break; case 'trash': case 'untrash': case 'delete': // Re-generate nonce if validation passes. check_admin_referer( $action . '-post_' . $post_id ); $new_url = add_query_arg( array( 'action' => $action, 'order' => array( $post_id ), '_wp_http_referer' => $this->page_controller->get_orders_url(), '_wpnonce' => wp_create_nonce( 'bulk-orders' ), ), $this->page_controller->get_orders_url() ); break; default: break; } if ( ! $new_url ) { return; } $new_url = add_query_arg( $query_args, $new_url ); wp_safe_redirect( $new_url, 301 ); exit; } } Orders/EditLock.php 0000644 00000017016 15073235522 0010222 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders; /** * This class takes care of the edit lock logic when HPOS is enabled. * For better interoperability with WordPress, edit locks are stored in the same format as posts. That is, as a metadata * in the order object (key: '_edit_lock') in the format "timestamp:user_id". * * @since 7.8.0 */ class EditLock { const META_KEY_NAME = '_edit_lock'; /** * Obtains lock information for a given order. If the lock has expired or it's assigned to an invalid user, * the order is no longer considered locked. * * @param \WC_Order $order Order to check. * @return bool|array */ public function get_lock( \WC_Order $order ) { $lock = $order->get_meta( self::META_KEY_NAME, true, 'edit' ); if ( ! $lock ) { return false; } $lock = explode( ':', $lock ); if ( 2 !== count( $lock ) ) { return false; } $time = absint( $lock[0] ); $user_id = isset( $lock[1] ) ? absint( $lock[1] ) : 0; if ( ! $time || ! get_user_by( 'id', $user_id ) ) { return false; } /** This filter is documented in WP's wp-admin/includes/ajax-actions.php */ $time_window = apply_filters( 'wp_check_post_lock_window', 150 ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment if ( time() >= ( $time + $time_window ) ) { return false; } return compact( 'time', 'user_id' ); } /** * Checks whether the order is being edited (i.e. locked) by another user. * * @param \WC_Order $order Order to check. * @return bool TRUE if order is locked and currently being edited by another user. FALSE otherwise. */ public function is_locked_by_another_user( \WC_Order $order ) : bool { $lock = $this->get_lock( $order ); return $lock && ( get_current_user_id() !== $lock['user_id'] ); } /** * Checks whether the order is being edited by any user. * * @param \WC_Order $order Order to check. * @return boolean TRUE if order is locked and currently being edited by a user. FALSE otherwise. */ public function is_locked( \WC_Order $order ) : bool { return (bool) $this->get_lock( $order ); } /** * Assigns an order's edit lock to the current user. * * @param \WC_Order $order The order to apply the lock to. * @return array|bool FALSE if no user is logged-in, an array in the same format as {@see get_lock()} otherwise. */ public function lock( \WC_Order $order ) { $user_id = get_current_user_id(); if ( ! $user_id ) { return false; } $order->update_meta_data( self::META_KEY_NAME, time() . ':' . $user_id ); $order->save_meta_data(); return $order->get_meta( self::META_KEY_NAME, true, 'edit' ); } /** * Hooked to 'heartbeat_received' on the edit order page to refresh the lock on an order being edited by the current user. * * @param array $response The heartbeat response to be sent. * @param array $data Data sent through the heartbeat. * @return array Response to be sent. */ public function refresh_lock_ajax( $response, $data ) { $order_id = absint( $data['wc-refresh-order-lock'] ?? 0 ); if ( ! $order_id ) { return $response; } unset( $response['wp-refresh-post-lock'] ); $order = wc_get_order( $order_id ); if ( ! $order || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) { return $response; } $response['wc-refresh-order-lock'] = array(); if ( ! $this->is_locked_by_another_user( $order ) ) { $response['wc-refresh-order-lock']['lock'] = $this->lock( $order ); } else { $current_lock = $this->get_lock( $order ); $user = get_user_by( 'id', $current_lock['user_id'] ); $response['wc-refresh-order-lock']['error'] = array( // translators: %s is a user's name. 'message' => sprintf( __( '%s has taken over and is currently editing.', 'woocommerce' ), $user->display_name ), 'user_name' => $user->display_name, 'user_avatar_src' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 64 ) ) : '', 'user_avatar_src_2x' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 128 ) ) : '', ); } return $response; } /** * Hooked to 'heartbeat_received' on the orders screen to refresh the locked status of orders in the list table. * * @param array $response The heartbeat response to be sent. * @param array $data Data sent through the heartbeat. * @return array Response to be sent. */ public function check_locked_orders_ajax( $response, $data ) { if ( empty( $data['wc-check-locked-orders'] ) || ! is_array( $data['wc-check-locked-orders'] ) ) { return $response; } $response['wc-check-locked-orders'] = array(); $order_ids = array_unique( array_map( 'absint', $data['wc-check-locked-orders'] ) ); foreach ( $order_ids as $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { continue; } if ( ! $this->is_locked_by_another_user( $order ) || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) { continue; } $response['wc-check-locked-orders'][ $order_id ] = true; } return $response; } /** * Outputs HTML for the lock dialog based on the status of the lock on the order (if any). * Depending on who owns the lock, this could be a message with the chance to take over or a message indicating that * someone else has taken over the order. * * @param \WC_Order $order Order object. * @return void */ public function render_dialog( $order ) { $lock = $this->get_lock( $order ); $user = $lock ? get_user_by( 'id', $lock['user_id'] ) : false; $locked = $user && ( get_current_user_id() !== $user->ID ); $edit_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_edit_url( $order->get_id() ); $sendback_url = wp_get_referer(); if ( ! $sendback_url ) { $sendback_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_base_page_url( $order->get_type() ); } $sendback_text = __( 'Go back', 'woocommerce' ); ?> <div id="post-lock-dialog" class="notification-dialog-wrap <?php echo $locked ? '' : 'hidden'; ?> order-lock-dialog"> <div class="notification-dialog-background"></div> <div class="notification-dialog"> <?php if ( $locked ) : ?> <div class="post-locked-message"> <div class="post-locked-avatar"><?php echo get_avatar( $user->ID, 64 ); ?></div> <p class="currently-editing wp-tab-first" tabindex="0"> <?php // translators: %s is a user's name. echo esc_html( sprintf( __( '%s is currently editing this order. Do you want to take over?', 'woocommerce' ), esc_html( $user->display_name ) ) ); ?> </p> <p> <a class="button" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a> <a class="button button-primary wp-tab-last" href="<?php echo esc_url( add_query_arg( 'claim-lock', '1', wp_nonce_url( $edit_url, 'claim-lock-' . $order->get_id() ) ) ); ?>"><?php esc_html_e( 'Take over', 'woocommerce' ); ?></a> </p> </div> <?php else : ?> <div class="post-taken-over"> <div class="post-locked-avatar"></div> <p class="wp-tab-first" tabindex="0"> <span class="currently-editing"></span><br /> </p> <p><a class="button button-primary wp-tab-last" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a></p> </div> <?php endif; ?> </div> </div> <?php } } Orders/PageController.php 0000644 00000037746 15073235522 0011460 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; /** * Controls the different pages/screens associated to the "Orders" menu page. */ class PageController { use AccessiblePrivateMethods; /** * The order type. * * @var string */ private $order_type = ''; /** * Instance of the posts redirection controller. * * @var PostsRedirectionController */ private $redirection_controller; /** * Instance of the orders list table. * * @var ListTable */ private $orders_table; /** * Instance of orders edit form. * * @var Edit */ private $order_edit_form; /** * Current action. * * @var string */ private $current_action = ''; /** * Order object to be used in edit/new form. * * @var \WC_Order */ private $order; /** * Verify that user has permission to edit orders. * * @return void */ private function verify_edit_permission() { if ( 'edit_order' === $this->current_action && ( ! isset( $this->order ) || ! $this->order ) ) { wp_die( esc_html__( 'You attempted to edit an order that does not exist. Perhaps it was deleted?', 'woocommerce' ) ); } if ( $this->order->get_type() !== $this->order_type ) { wp_die( esc_html__( 'Order type mismatch.', 'woocommerce' ) ); } if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->edit_post, $this->order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) ); } if ( 'trash' === $this->order->get_status() ) { wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) ); } } /** * Verify that user has permission to create order. * * @return void */ private function verify_create_permission() { if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->publish_posts ) && ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You don\'t have permission to create a new order', 'woocommerce' ) ); } if ( isset( $this->order ) ) { $this->verify_edit_permission(); } } /** * Claims the lock for the order being edited/created (unless it belongs to someone else). * Also handles the 'claim-lock' action which allows taking over the order forcefully. * * @return void */ private function handle_edit_lock() { if ( ! $this->order ) { return; } $edit_lock = wc_get_container()->get( EditLock::class ); $locked = $edit_lock->is_locked_by_another_user( $this->order ); // Take over order? if ( ! empty( $_GET['claim-lock'] ) && wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'claim-lock-' . $this->order->get_id() ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash $edit_lock->lock( $this->order ); wp_safe_redirect( $this->get_edit_url( $this->order->get_id() ) ); exit; } if ( ! $locked ) { $edit_lock->lock( $this->order ); } add_action( 'admin_footer', function() use ( $edit_lock ) { $edit_lock->render_dialog( $this->order ); } ); } /** * Sets up the page controller, including registering the menu item. * * @return void */ public function setup(): void { global $plugin_page, $pagenow; $this->redirection_controller = new PostsRedirectionController( $this ); // Register menu. if ( 'admin_menu' === current_action() ) { $this->register_menu(); } else { add_action( 'admin_menu', 'register_menu', 9 ); } // Not on an Orders page. if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) { return; } $this->set_order_type(); $this->set_action(); $page_suffix = ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type ); self::add_action( 'load-woocommerce_page_wc-orders' . $page_suffix, array( $this, 'handle_load_page_action' ) ); self::add_action( 'admin_title', array( $this, 'set_page_title' ) ); } /** * Perform initialization for the current action. */ private function handle_load_page_action() { $screen = get_current_screen(); $screen->post_type = $this->order_type; if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) { $this->{"setup_action_{$this->current_action}"}(); } } /** * Set the document title for Orders screens to match what it would be with the shop_order CPT. * * @param string $admin_title The admin screen title before it's filtered. * * @return string The filtered admin title. */ private function set_page_title( $admin_title ) { if ( ! $this->is_order_screen( $this->order_type ) ) { return $admin_title; } $wp_order_type = get_post_type_object( $this->order_type ); $labels = get_post_type_labels( $wp_order_type ); if ( $this->is_order_screen( $this->order_type, 'list' ) ) { $admin_title = sprintf( // translators: 1: The label for an order type 2: The name of the website. esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ), esc_html( $labels->name ), esc_html( get_bloginfo( 'name' ) ) ); } elseif ( $this->is_order_screen( $this->order_type, 'edit' ) ) { $admin_title = sprintf( // translators: 1: The label for an order type 2: The title of the order 3: The name of the website. esc_html__( '%1$s #%2$s ‹ %3$s — WordPress', 'woocommerce' ), esc_html( $labels->edit_item ), absint( $this->order->get_id() ), esc_html( get_bloginfo( 'name' ) ) ); } elseif ( $this->is_order_screen( $this->order_type, 'new' ) ) { $admin_title = sprintf( // translators: 1: The label for an order type 2: The name of the website. esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ), esc_html( $labels->add_new_item ), esc_html( get_bloginfo( 'name' ) ) ); } return $admin_title; } /** * Determines the order type for the current screen. * * @return void */ private function set_order_type() { global $plugin_page; $this->order_type = str_replace( array( 'wc-orders--', 'wc-orders' ), '', $plugin_page ); $this->order_type = empty( $this->order_type ) ? 'shop_order' : $this->order_type; $wc_order_type = wc_get_order_type( $this->order_type ); $wp_order_type = get_post_type_object( $this->order_type ); if ( ! $wc_order_type || ! $wp_order_type || ! $wp_order_type->show_ui || ! current_user_can( $wp_order_type->cap->edit_posts ) ) { wp_die(); } } /** * Sets the current action based on querystring arguments. Defaults to 'list_orders'. * * @return void */ private function set_action(): void { switch ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) { case 'edit': $this->current_action = 'edit_order'; break; case 'new': $this->current_action = 'new_order'; break; default: $this->current_action = 'list_orders'; break; } } /** * Registers the "Orders" menu. * * @return void */ public function register_menu(): void { $order_types = wc_get_order_types( 'admin-menu' ); foreach ( $order_types as $order_type ) { $post_type = get_post_type_object( $order_type ); add_submenu_page( 'woocommerce', $post_type->labels->name, $post_type->labels->menu_name, $post_type->cap->edit_posts, 'wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ), array( $this, 'output' ) ); } // In some cases (such as if the authoritative order store was changed earlier in the current request) we // need an extra step to remove the menu entry for the menu post type. add_action( 'admin_init', function() use ( $order_types ) { foreach ( $order_types as $order_type ) { remove_submenu_page( 'woocommerce', 'edit.php?post_type=' . $order_type ); } } ); } /** * Outputs content for the current orders screen. * * @return void */ public function output(): void { switch ( $this->current_action ) { case 'edit_order': case 'new_order': $this->order_edit_form->display(); break; case 'list_orders': default: $this->orders_table->prepare_items(); $this->orders_table->display(); break; } } /** * Handles initialization of the orders list table. * * @return void */ private function setup_action_list_orders(): void { $this->orders_table = wc_get_container()->get( ListTable::class ); $this->orders_table->setup( array( 'order_type' => $this->order_type, ) ); if ( $this->orders_table->current_action() ) { $this->orders_table->handle_bulk_actions(); } $this->strip_http_referer(); } /** * Perform a redirect to remove the `_wp_http_referer` and `_wpnonce` strings if present in the URL (see also * wp-admin/edit.php where a similar process takes place), otherwise the size of this field builds to an * unmanageable length over time. */ private function strip_http_referer(): void { $current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ); $stripped_url = remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), $current_url ); if ( $stripped_url !== $current_url ) { wp_safe_redirect( $stripped_url ); exit; } } /** * Prepares the order edit form for creating or editing an order. * * @see \Automattic\WooCommerce\Internal\Admin\Orders\Edit. * @since 8.1.0 */ private function prepare_order_edit_form(): void { if ( ! $this->order || ! in_array( $this->current_action, array( 'new_order', 'edit_order' ), true ) ) { return; } $this->order_edit_form = $this->order_edit_form ?? new Edit(); $this->order_edit_form->setup( $this->order ); $this->order_edit_form->set_current_action( $this->current_action ); } /** * Handles initialization of the orders edit form. * * @return void */ private function setup_action_edit_order(): void { global $theorder; $this->order = wc_get_order( absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 ) ); $this->verify_edit_permission(); $this->handle_edit_lock(); $theorder = $this->order; $this->prepare_order_edit_form(); } /** * Handles initialization of the orders edit form with a new order. * * @return void */ private function setup_action_new_order(): void { global $theorder; $this->verify_create_permission(); $order_class_name = wc_get_order_type( $this->order_type )['class_name']; if ( ! $order_class_name || ! class_exists( $order_class_name ) ) { wp_die(); } $this->order = new $order_class_name(); $this->order->set_object_read( false ); $this->order->set_status( 'auto-draft' ); $this->order->set_created_via( 'admin' ); $this->order->save(); $this->handle_edit_lock(); // Schedule auto-draft cleanup. We re-use the WP event here on purpose. if ( ! wp_next_scheduled( 'wp_scheduled_auto_draft_delete' ) ) { wp_schedule_event( time(), 'daily', 'wp_scheduled_auto_draft_delete' ); } $theorder = $this->order; $this->prepare_order_edit_form(); } /** * Returns the current order type. * * @return string */ public function get_order_type() { return $this->order_type; } /** * Helper method to generate a link to the main orders screen. * * @return string Orders screen URL. */ public function get_orders_url(): string { return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ? admin_url( 'admin.php?page=wc-orders' ) : admin_url( 'edit.php?post_type=shop_order' ); } /** * Helper method to generate edit link for an order. * * @param int $order_id Order ID. * * @return string Edit link. */ public function get_edit_url( int $order_id ) : string { if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { return admin_url( 'post.php?post=' . absint( $order_id ) ) . '&action=edit'; } $order = wc_get_order( $order_id ); // Confirm we could obtain the order object (since it's possible it will not exist, due to a sync issue, or may // have been deleted in a separate concurrent request). if ( false === $order ) { wc_get_logger()->debug( sprintf( /* translators: %d order ID. */ __( 'Attempted to determine the edit URL for order %d, however the order does not exist.', 'woocommerce' ), $order_id ) ); $order_type = 'shop_order'; } else { $order_type = $order->get_type(); } return add_query_arg( array( 'action' => 'edit', 'id' => absint( $order_id ), ), $this->get_base_page_url( $order_type ) ); } /** * Helper method to generate a link for creating order. * * @param string $order_type The order type. Defaults to 'shop_order'. * @return string */ public function get_new_page_url( $order_type = 'shop_order' ) : string { $url = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ? add_query_arg( 'action', 'new', $this->get_base_page_url( $order_type ) ) : admin_url( 'post-new.php?post_type=' . $order_type ); return $url; } /** * Helper method to generate a link to the main screen for a custom order type. * * @param string $order_type The order type. * * @return string * * @throws \Exception When an invalid order type is passed. */ public function get_base_page_url( $order_type ): string { $order_types_with_ui = wc_get_order_types( 'admin-menu' ); if ( ! in_array( $order_type, $order_types_with_ui, true ) ) { // translators: %s is a custom order type. throw new \Exception( sprintf( __( 'Invalid order type: %s.', 'woocommerce' ), esc_html( $order_type ) ) ); } return admin_url( 'admin.php?page=wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ) ); } /** * Helper method to check if the current admin screen is related to orders. * * @param string $type Optional. The order type to check for. Default shop_order. * @param string $action Optional. The purpose of the screen to check for. 'list', 'edit', or 'new'. * Leave empty to check for any order screen. * * @return bool */ public function is_order_screen( $type = 'shop_order', $action = '' ) : bool { if ( ! did_action( 'current_screen' ) ) { wc_doing_it_wrong( __METHOD__, sprintf( // translators: %s is the name of a function. esc_html__( '%s must be called after the current_screen action.', 'woocommerce' ), esc_html( __METHOD__ ) ), '7.9.0' ); return false; } $valid_types = wc_get_order_types( 'view-order' ); if ( ! in_array( $type, $valid_types, true ) ) { wc_doing_it_wrong( __METHOD__, sprintf( // translators: %s is the name of an order type. esc_html__( '%s is not a valid order type.', 'woocommerce' ), esc_html( $type ) ), '7.9.0' ); return false; } if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { if ( $action ) { switch ( $action ) { case 'edit': $is_action = 'edit_order' === $this->current_action; break; case 'list': $is_action = 'list_orders' === $this->current_action; break; case 'new': $is_action = 'new_order' === $this->current_action; break; default: $is_action = false; break; } } $type_match = $type === $this->order_type; $action_match = ! $action || $is_action; } else { $screen = get_current_screen(); if ( $action ) { switch ( $action ) { case 'edit': $screen_match = 'post' === $screen->base && filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT ); break; case 'list': $screen_match = 'edit' === $screen->base; break; case 'new': $screen_match = 'post' === $screen->base && 'add' === $screen->action; break; default: $screen_match = false; break; } } $type_match = $type === $screen->post_type; $action_match = ! $action || $screen_match; } return $type_match && $action_match; } } WCPayPromotion/WCPayPromotionDataSourcePoller.php 0000644 00000001425 15073235522 0016200 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion; use Automattic\WooCommerce\Admin\DataSourcePoller; /** * Specs data source poller class for WooCommerce Payment Promotion. */ class WCPayPromotionDataSourcePoller extends DataSourcePoller { const ID = 'payment_method_promotion'; /** * Default data sources array. */ const DATA_SOURCES = array( 'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/payment-method/promotions.json', ); /** * Class instance. * * @var Analytics instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self( self::ID, self::DATA_SOURCES ); } return self::$instance; } } WCPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php 0000644 00000003224 15073235522 0020156 0 ustar 00 <?php /** * Class WCPaymentGatewayPreInstallWCPayPromotion * * @package WooCommerce\Admin */ namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * A Psuedo WCPay gateway class. * * @extends WC_Payment_Gateway */ class WCPaymentGatewayPreInstallWCPayPromotion extends \WC_Payment_Gateway { const GATEWAY_ID = 'pre_install_woocommerce_payments_promotion'; /** * Constructor */ public function __construct() { $wc_pay_spec = Init::get_wc_pay_promotion_spec(); if ( ! $wc_pay_spec ) { return; } $this->id = static::GATEWAY_ID; $this->method_title = $wc_pay_spec->title; if ( property_exists( $wc_pay_spec, 'sub_title' ) ) { $this->title = sprintf( '<span class="gateway-subtitle" >%s</span>', $wc_pay_spec->sub_title ); } $this->method_description = $wc_pay_spec->content; $this->has_fields = false; // Get setting values. $this->enabled = false; // Load the settings. $this->init_form_fields(); $this->init_settings(); } /** * Initialise Gateway Settings Form Fields. */ public function init_form_fields() { $this->form_fields = array( 'is_dismissed' => array( 'title' => __( 'Dismiss', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Dismiss the gateway', 'woocommerce' ), 'default' => 'no', ), ); } /** * Check if the promotional gateaway has been dismissed. * * @return bool */ public static function is_dismissed() { $settings = get_option( 'woocommerce_' . self::GATEWAY_ID . '_settings', array() ); return isset( $settings['is_dismissed'] ) && 'yes' === $settings['is_dismissed']; } } WCPayPromotion/Init.php 0000644 00000012436 15073235522 0011064 0 ustar 00 <?php /** * Handles wcpay promotion */ namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\DataSourcePoller; use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion; use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller as PaymentGatewaySuggestionsDataSourcePoller; use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; /** * WC Pay Promotion engine. */ class Init { const EXPLAT_VARIATION_PREFIX = 'woocommerce_wc_pay_promotion_payment_methods_table_'; /** * Constructor. */ public function __construct() { include_once __DIR__ . '/WCPaymentGatewayPreInstallWCPayPromotion.php'; $is_payments_page = isset( $_GET['page'] ) && $_GET['page'] === 'wc-settings' && isset( $_GET['tab'] ) && $_GET['tab'] === 'checkout'; // phpcs:ignore WordPress.Security.NonceVerification if ( ! wp_is_json_request() && ! $is_payments_page ) { return; } add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'possibly_register_pre_install_wc_pay_promotion_gateway' ) ); add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] ); $rtl = is_rtl() ? '.rtl' : ''; wp_enqueue_style( 'wc-admin-payment-method-promotions', WCAdminAssets::get_url( "payment-method-promotions/style{$rtl}", 'css' ), array( 'wp-components' ), WCAdminAssets::get_file_version( 'css' ) ); WCAdminAssets::register_script( 'wp-admin-scripts', 'payment-method-promotions', true ); } /** * Possibly registers the pre install wc pay promoted gateway. * * @param array $gateways list of gateway classes. * @return array list of gateway classes. */ public static function possibly_register_pre_install_wc_pay_promotion_gateway( $gateways ) { if ( self::can_show_promotion() && ! WCPaymentGatewayPreInstallWCPayPromotion::is_dismissed() ) { $gateways[] = 'Automattic\WooCommerce\Internal\Admin\WCPayPromotion\WCPaymentGatewayPreInstallWCPayPromotion'; } return $gateways; } /** * Checks if promoted gateway can be registered. * * @return boolean if promoted gateway should be registered. */ public static function can_show_promotion() { // Check if WC Pay is enabled. if ( class_exists( '\WC_Payments' ) ) { return false; } if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) { return false; } if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) { return false; } $wc_pay_spec = self::get_wc_pay_promotion_spec(); if ( ! $wc_pay_spec ) { return false; } return true; } /** * By default, new payment gateways are put at the bottom of the list on the admin "Payments" settings screen. * For visibility, we want WooCommerce Payments to be at the top of the list. * * @param array $ordering Existing ordering of the payment gateways. * * @return array Modified ordering. */ public static function set_gateway_top_of_list( $ordering ) { $ordering = (array) $ordering; $id = WCPaymentGatewayPreInstallWCPayPromotion::GATEWAY_ID; // Only tweak the ordering if the list hasn't been reordered with WooCommerce Payments in it already. if ( ! isset( $ordering[ $id ] ) || ! is_numeric( $ordering[ $id ] ) ) { $is_empty = empty( $ordering ) || ( count( $ordering ) === 1 && $ordering[0] === false ); $ordering[ $id ] = $is_empty ? 0 : ( min( $ordering ) - 1 ); } return $ordering; } /** * Get WC Pay promotion spec. */ public static function get_wc_pay_promotion_spec() { $promotions = self::get_promotions(); $wc_pay_promotion_spec = array_values( array_filter( $promotions, function( $promotion ) { return isset( $promotion->plugins ) && in_array( 'woocommerce-payments', $promotion->plugins, true ); } ) ); return current( $wc_pay_promotion_spec ); } /** * Go through the specs and run them. */ public static function get_promotions() { $suggestions = array(); $specs = self::get_specs(); foreach ( $specs as $spec ) { try { $suggestion = EvaluateSuggestion::evaluate( $spec ); $suggestions[] = $suggestion; // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch } catch ( \Throwable $e ) { // Ignore errors. } } return array_values( array_filter( $suggestions, function( $suggestion ) { return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible; } ) ); } /** * Get merchant WooPay eligibility. */ public static function is_woopay_eligible() { $wcpay_promotion = self::get_wc_pay_promotion_spec(); return $wcpay_promotion && 'woocommerce_payments:woopay' === $wcpay_promotion->id; } /** * Delete the specs transient. */ public static function delete_specs_transient() { WCPayPromotionDataSourcePoller::get_instance()->delete_specs_transient(); } /** * Get specs or fetch remotely if they don't exist. */ public static function get_specs() { if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) { return array(); } return WCPayPromotionDataSourcePoller::get_instance()->get_specs_from_data_sources(); } } Analytics.php 0000644 00000021702 15073235522 0007212 0 ustar 00 <?php /** * WooCommerce Analytics. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\API\Reports\Cache; use Automattic\WooCommerce\Admin\Features\Features; /** * Contains backend logic for the Analytics feature. */ class Analytics { /** * Option name used to toggle this feature. */ const TOGGLE_OPTION_NAME = 'woocommerce_analytics_enabled'; /** * Clear cache tool identifier. */ const CACHE_TOOL_ID = 'clear_woocommerce_analytics_cache'; /** * Class instance. * * @var Analytics instance */ protected static $instance = null; /** * Determines if the feature has been toggled on or off. * * @var boolean */ protected static $is_updated = false; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 ); add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) ); if ( ! Features::is_enabled( 'analytics' ) ) { return; } add_filter( 'woocommerce_component_settings_preload_endpoints', array( $this, 'add_preload_endpoints' ) ); add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) ); add_action( 'admin_menu', array( $this, 'register_pages' ) ); add_filter( 'woocommerce_debug_tools', array( $this, 'register_cache_clear_tool' ) ); } /** * Add the feature toggle to the features settings. * * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class). * * @param array $features Feature sections. * @return array */ public static function add_feature_toggle( $features ) { return $features; } /** * Reloads the page when the option is toggled to make sure all Analytics features are loaded. * * @param string $old_value Old value. * @param string $value New value. */ public static function reload_page_on_toggle( $old_value, $value ) { if ( $old_value === $value ) { return; } self::$is_updated = true; } /** * Reload the page if the setting has been updated. */ public static function maybe_reload_page() { if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) { return; } wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) ); exit(); } /** * Preload data from the countries endpoint. * * @param array $endpoints Array of preloaded endpoints. * @return array */ public function add_preload_endpoints( $endpoints ) { $endpoints['performanceIndicators'] = '/wc-analytics/reports/performance-indicators/allowed'; $endpoints['leaderboards'] = '/wc-analytics/leaderboards/allowed'; return $endpoints; } /** * Adds fields so that we can store user preferences for the columns to display on a report. * * @param array $user_data_fields User data fields. * @return array */ public function add_user_data_fields( $user_data_fields ) { return array_merge( $user_data_fields, array( 'categories_report_columns', 'coupons_report_columns', 'customers_report_columns', 'orders_report_columns', 'products_report_columns', 'revenue_report_columns', 'taxes_report_columns', 'variations_report_columns', 'dashboard_sections', 'dashboard_chart_type', 'dashboard_chart_interval', 'dashboard_leaderboard_rows', ) ); } /** * Register the cache clearing tool on the WooCommerce > Status > Tools page. * * @param array $debug_tools Available debug tool registrations. * @return array Filtered debug tool registrations. */ public function register_cache_clear_tool( $debug_tools ) { $settings_url = add_query_arg( array( 'page' => 'wc-admin', 'path' => '/analytics/settings', ), get_admin_url( null, 'admin.php' ) ); $debug_tools[ self::CACHE_TOOL_ID ] = array( 'name' => __( 'Clear analytics cache', 'woocommerce' ), 'button' => __( 'Clear', 'woocommerce' ), 'desc' => sprintf( /* translators: 1: opening link tag, 2: closing tag */ __( 'This tool will reset the cached values used in WooCommerce Analytics. If numbers still look off, try %1$sReimporting Historical Data%2$s.', 'woocommerce' ), '<a href="' . esc_url( $settings_url ) . '">', '</a>' ), 'callback' => array( $this, 'run_clear_cache_tool' ), ); return $debug_tools; } /** * Registers report pages. */ public function register_pages() { $report_pages = self::get_report_pages(); foreach ( $report_pages as $report_page ) { if ( ! is_null( $report_page ) ) { wc_admin_register_page( $report_page ); } } } /** * Get report pages. */ public static function get_report_pages() { $overview_page = array( 'id' => 'woocommerce-analytics', 'title' => __( 'Analytics', 'woocommerce' ), 'path' => '/analytics/overview', 'icon' => 'dashicons-chart-bar', 'position' => 57, // After WooCommerce & Product menu items. ); $report_pages = array( $overview_page, array( 'id' => 'woocommerce-analytics-overview', 'title' => __( 'Overview', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/overview', 'nav_args' => array( 'order' => 10, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-products', 'title' => __( 'Products', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/products', 'nav_args' => array( 'order' => 20, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-revenue', 'title' => __( 'Revenue', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/revenue', 'nav_args' => array( 'order' => 30, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-orders', 'title' => __( 'Orders', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/orders', 'nav_args' => array( 'order' => 40, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-variations', 'title' => __( 'Variations', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/variations', 'nav_args' => array( 'order' => 50, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-categories', 'title' => __( 'Categories', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/categories', 'nav_args' => array( 'order' => 60, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-coupons', 'title' => __( 'Coupons', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/coupons', 'nav_args' => array( 'order' => 70, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-taxes', 'title' => __( 'Taxes', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/taxes', 'nav_args' => array( 'order' => 80, 'parent' => 'woocommerce-analytics', ), ), array( 'id' => 'woocommerce-analytics-downloads', 'title' => __( 'Downloads', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/downloads', 'nav_args' => array( 'order' => 90, 'parent' => 'woocommerce-analytics', ), ), 'yes' === get_option( 'woocommerce_manage_stock' ) ? array( 'id' => 'woocommerce-analytics-stock', 'title' => __( 'Stock', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/stock', 'nav_args' => array( 'order' => 100, 'parent' => 'woocommerce-analytics', ), ) : null, array( 'id' => 'woocommerce-analytics-customers', 'title' => __( 'Customers', 'woocommerce' ), 'parent' => 'woocommerce', 'path' => '/customers', ), array( 'id' => 'woocommerce-analytics-settings', 'title' => __( 'Settings', 'woocommerce' ), 'parent' => 'woocommerce-analytics', 'path' => '/analytics/settings', 'nav_args' => array( 'title' => __( 'Analytics', 'woocommerce' ), 'parent' => 'woocommerce-settings', ), ), ); /** * The analytics report items used in the menu. * * @since 6.4.0 */ return apply_filters( 'woocommerce_analytics_report_menu_items', $report_pages ); } /** * "Clear" analytics cache by invalidating it. */ public function run_clear_cache_tool() { Cache::invalidate(); return __( 'Analytics cache cleared.', 'woocommerce' ); } } Logging/FileV2/FileController.php 0000644 00000044743 15073235522 0012675 0 ustar 00 <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2; use Automattic\Jetpack\Constants; use PclZip; use WC_Cache_Helper; use WP_Error; /** * FileController class. */ class FileController { /** * The maximum number of rotations for a file before they start getting overwritten. * * This number should not go above 10, or it will cause issues with the glob patterns. * * const int */ private const MAX_FILE_ROTATIONS = 10; /** * Default values for arguments for the get_files method. * * @const array */ public const DEFAULTS_GET_FILES = array( 'date_end' => 0, 'date_filter' => '', 'date_start' => 0, 'offset' => 0, 'order' => 'desc', 'orderby' => 'modified', 'per_page' => 20, 'source' => '', ); /** * Default values for arguments for the search_within_files method. * * @const array */ public const DEFAULTS_SEARCH_WITHIN_FILES = array( 'offset' => 0, 'per_page' => 50, ); /** * The maximum number of files that can be searched at one time. * * @const int */ public const SEARCH_MAX_FILES = 100; /** * The maximum number of search results that can be returned at one time. * * @const int */ public const SEARCH_MAX_RESULTS = 200; /** * The cache group name to use for caching operations. * * @const string */ private const CACHE_GROUP = 'log-files'; /** * A cache key for storing and retrieving the results of the last logs search. * * @const string */ private const SEARCH_CACHE_KEY = 'logs_previous_search'; /** * The absolute path to the log directory. * * @var string */ private $log_directory; /** * Class FileController */ public function __construct() { $this->log_directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) ); } /** * Get the file size limit that determines when to rotate a file. * * @return int */ private function get_file_size_limit(): int { $default = 5 * MB_IN_BYTES; /** * Filter the threshold size of a log file at which point it will get rotated. * * @since 3.4.0 * * @param int $file_size_limit The file size limit in bytes. */ $file_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $default ); if ( ! is_int( $file_size_limit ) || $file_size_limit < 1 ) { return $default; } return $file_size_limit; } /** * Write a log entry to the appropriate file, after rotating the file if necessary. * * @param string $source The source property of the log entry, which determines which file to write to. * @param string $text The contents of the log entry to add to a file. * @param int|null $time Optional. The time of the log entry as a Unix timestamp. Defaults to the current time. * * @return bool True if the contents were successfully written to the file. */ public function write_to_file( string $source, string $text, ?int $time = null ): bool { if ( is_null( $time ) ) { $time = time(); } $file_id = File::generate_file_id( $source, null, $time ); $file = $this->get_file_by_id( $file_id ); if ( $file instanceof File && $file->get_file_size() >= $this->get_file_size_limit() ) { $rotated = $this->rotate_file( $file->get_file_id() ); if ( $rotated ) { $file = null; } else { return false; } } if ( ! $file instanceof File ) { $new_path = $this->log_directory . $this->generate_filename( $source, $time ); $file = new File( $new_path ); } return $file->write( $text ); } /** * Generate the full name of a file based on source and date values. * * @param string $source The source property of a log entry, which determines the filename. * @param int $time The time of the log entry as a Unix timestamp. * * @return string */ private function generate_filename( string $source, int $time ): string { $file_id = File::generate_file_id( $source, null, $time ); $hash = File::generate_hash( $file_id ); return "$file_id-$hash.log"; } /** * Get all the rotations of a file and increment them, so that they overwrite the previous file with that rotation. * * @param string $file_id A file ID (file basename without the hash). * * @return bool True if the file and all its rotations were successfully rotated. */ private function rotate_file( $file_id ): bool { $rotations = $this->get_file_rotations( $file_id ); if ( is_wp_error( $rotations ) || ! isset( $rotations['current'] ) ) { return false; } $max_rotation_marker = self::MAX_FILE_ROTATIONS - 1; // Don't rotate a file with the maximum rotation. unset( $rotations[ $max_rotation_marker ] ); $results = array(); // Rotate starting with oldest first and working backwards. for ( $i = $max_rotation_marker; $i >= 0; $i -- ) { if ( isset( $rotations[ $i ] ) ) { $results[] = $rotations[ $i ]->rotate(); } } $results[] = $rotations['current']->rotate(); return ! in_array( false, $results, true ); } /** * Get an array of log files. * * @param array $args { * Optional. Arguments to filter and sort the files that are returned. * * @type int $date_end The end of the date range to filter by, as a Unix timestamp. * @type string $date_filter Filter files by one of the date props. 'created' or 'modified'. * @type int $date_start The beginning of the date range to filter by, as a Unix timestamp. * @type int $offset Omit this number of files from the beginning of the list. Works with $per_page to do pagination. * @type string $order The sort direction. 'asc' or 'desc'. Defaults to 'desc'. * @type string $orderby The property to sort the list by. 'created', 'modified', 'source', 'size'. Defaults to 'modified'. * @type int $per_page The number of files to include in the list. Works with $offset to do pagination. * @type string $source Only include files from this source. * } * @param bool $count_only Optional. True to return a total count of the files. * * @return File[]|int|WP_Error */ public function get_files( array $args = array(), bool $count_only = false ) { $args = wp_parse_args( $args, self::DEFAULTS_GET_FILES ); $pattern = $args['source'] . '*.log'; $paths = glob( $this->log_directory . $pattern ); if ( false === $paths ) { return new WP_Error( 'wc_log_directory_error', __( 'Could not access the log file directory.', 'woocommerce' ) ); } $files = $this->convert_paths_to_objects( $paths ); if ( $args['date_filter'] && $args['date_start'] && $args['date_end'] ) { switch ( $args['date_filter'] ) { case 'created': $files = array_filter( $files, fn( $file ) => $file->get_created_timestamp() >= $args['date_start'] && $file->get_created_timestamp() <= $args['date_end'] ); break; case 'modified': $files = array_filter( $files, fn( $file ) => $file->get_modified_timestamp() >= $args['date_start'] && $file->get_modified_timestamp() <= $args['date_end'] ); break; } } if ( true === $count_only ) { return count( $files ); } $multi_sorter = function( $sort_sets, $order_sets ) { $comparison = 0; while ( ! empty( $sort_sets ) ) { $set = array_shift( $sort_sets ); $order = array_shift( $order_sets ); if ( 'desc' === $order ) { $comparison = $set[1] <=> $set[0]; } else { $comparison = $set[0] <=> $set[1]; } if ( 0 !== $comparison ) { break; } } return $comparison; }; switch ( $args['orderby'] ) { case 'created': $sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) { $sort_sets = array( array( $a->get_created_timestamp(), $b->get_created_timestamp() ), array( $a->get_source(), $b->get_source() ), array( $a->get_rotation() || -1, $b->get_rotation() || -1 ), ); $order_sets = array( $args['order'], 'asc', 'asc' ); return $multi_sorter( $sort_sets, $order_sets ); }; break; case 'modified': $sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) { $sort_sets = array( array( $a->get_modified_timestamp(), $b->get_modified_timestamp() ), array( $a->get_source(), $b->get_source() ), array( $a->get_rotation() || -1, $b->get_rotation() || -1 ), ); $order_sets = array( $args['order'], 'asc', 'asc' ); return $multi_sorter( $sort_sets, $order_sets ); }; break; case 'source': $sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) { $sort_sets = array( array( $a->get_source(), $b->get_source() ), array( $a->get_created_timestamp(), $b->get_created_timestamp() ), array( $a->get_rotation() || -1, $b->get_rotation() || -1 ), ); $order_sets = array( $args['order'], 'desc', 'asc' ); return $multi_sorter( $sort_sets, $order_sets ); }; break; case 'size': $sort_callback = function( $a, $b ) use ( $args, $multi_sorter ) { $sort_sets = array( array( $a->get_file_size(), $b->get_file_size() ), array( $a->get_source(), $b->get_source() ), array( $a->get_rotation() || -1, $b->get_rotation() || -1 ), ); $order_sets = array( $args['order'], 'asc', 'asc' ); return $multi_sorter( $sort_sets, $order_sets ); }; break; } usort( $files, $sort_callback ); return array_slice( $files, $args['offset'], $args['per_page'] ); } /** * Get one or more File instances from an array of file IDs. * * @param array $file_ids An array of file IDs (file basename without the hash). * * @return File[] */ public function get_files_by_id( array $file_ids ): array { $paths = array(); foreach ( $file_ids as $file_id ) { // Look for the standard filename format first, which includes a hash. $glob = glob( $this->log_directory . $file_id . '-*.log' ); if ( ! $glob ) { $glob = glob( $this->log_directory . $file_id . '.log' ); } if ( is_array( $glob ) ) { $paths = array_merge( $paths, $glob ); } } $files = $this->convert_paths_to_objects( array_unique( $paths ) ); return $files; } /** * Get a File instance from a file ID. * * @param string $file_id A file ID (file basename without the hash). * * @return File|WP_Error */ public function get_file_by_id( string $file_id ) { $result = $this->get_files_by_id( array( $file_id ) ); if ( count( $result ) < 1 ) { return new WP_Error( 'wc_log_file_error', esc_html__( 'This file does not exist.', 'woocommerce' ) ); } if ( count( $result ) > 1 ) { return new WP_Error( 'wc_log_file_error', esc_html__( 'Multiple files match this ID.', 'woocommerce' ) ); } return reset( $result ); } /** * Get File instances for a given file ID and all of its related rotations. * * @param string $file_id A file ID (file basename without the hash). * * @return File[]|WP_Error An associative array where the rotation integer of the file is the key, and a "current" * key for the iteration of the file that hasn't been rotated (if it exists). */ public function get_file_rotations( string $file_id ) { $file = $this->get_file_by_id( $file_id ); if ( is_wp_error( $file ) ) { return $file; } $current = array(); $rotations = array(); $source = $file->get_source(); $created = 0; if ( $file->has_standard_filename() ) { $created = $file->get_created_timestamp(); } if ( is_null( $file->get_rotation() ) ) { $current['current'] = $file; } else { $current_file_id = File::generate_file_id( $source, null, $created ); $result = $this->get_file_by_id( $current_file_id ); if ( ! is_wp_error( $result ) ) { $current['current'] = $result; } } $rotations_pattern = sprintf( '.[%s]', implode( '', range( 0, self::MAX_FILE_ROTATIONS - 1 ) ) ); $created_pattern = $created ? '-' . gmdate( 'Y-m-d', $created ) . '-' : ''; $rotation_pattern = $this->log_directory . $source . $rotations_pattern . $created_pattern . '*.log'; $rotation_paths = glob( $rotation_pattern ); $rotation_files = $this->convert_paths_to_objects( $rotation_paths ); foreach ( $rotation_files as $rotation_file ) { if ( $rotation_file->is_readable() ) { $rotations[ $rotation_file->get_rotation() ] = $rotation_file; } } ksort( $rotations ); return array_merge( $current, $rotations ); } /** * Helper method to get an array of File instances. * * @param array $paths An array of absolute file paths. * * @return File[] */ private function convert_paths_to_objects( array $paths ): array { $files = array_map( function( $path ) { $file = new File( $path ); return $file->is_readable() ? $file : null; }, $paths ); return array_filter( $files ); } /** * Get a list of sources for existing log files. * * @return array|WP_Error */ public function get_file_sources() { $paths = glob( $this->log_directory . '*.log' ); if ( false === $paths ) { return new WP_Error( 'wc_log_directory_error', __( 'Could not access the log file directory.', 'woocommerce' ) ); } $all_sources = array_map( function( $path ) { $file = new File( $path ); return $file->is_readable() ? $file->get_source() : null; }, $paths ); return array_unique( array_filter( $all_sources ) ); } /** * Delete one or more files from the filesystem. * * @param array $file_ids An array of file IDs (file basename without the hash). * * @return int The number of files that were deleted. */ public function delete_files( array $file_ids ): int { $deleted = 0; $files = $this->get_files_by_id( $file_ids ); foreach ( $files as $file ) { $result = false; if ( $file->is_writable() ) { $result = $file->delete(); } if ( true === $result ) { $deleted ++; } } if ( $deleted > 0 ) { $this->invalidate_cache(); } return $deleted; } /** * Stream a single file to the browser without zipping it first. * * @param string $file_id A file ID (file basename without the hash). * * @return WP_Error|void Only returns something if there is an error. */ public function export_single_file( $file_id ) { $file = $this->get_file_by_id( $file_id ); if ( is_wp_error( $file ) ) { return $file; } $file_name = $file->get_file_id() . '.log'; $exporter = new FileExporter( $file->get_path(), $file_name ); return $exporter->emit_file(); } /** * Create a zip archive of log files and stream it to the browser. * * @param array $file_ids An array of file IDs (file basename without the hash). * * @return WP_Error|void Only returns something if there is an error. */ public function export_multiple_files( array $file_ids ) { $files = $this->get_files_by_id( $file_ids ); if ( count( $files ) < 1 ) { return new WP_Error( 'wc_logs_invalid_file', __( 'Could not access the specified files.', 'woocommerce' ) ); } $temp_dir = get_temp_dir(); if ( ! is_dir( $temp_dir ) || ! wp_is_writable( $temp_dir ) ) { return new WP_Error( 'wc_logs_invalid_directory', __( 'Could not write to the temp directory. Try downloading files one at a time instead.', 'woocommerce' ) ); } require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; $path = trailingslashit( $temp_dir ) . 'woocommerce_logs_' . gmdate( 'Y-m-d_H-i-s' ) . '.zip'; $file_paths = array_map( fn( $file ) => $file->get_path(), $files ); $archive = new PclZip( $path ); $archive->create( $file_paths, PCLZIP_OPT_REMOVE_ALL_PATH ); $exporter = new FileExporter( $path ); return $exporter->emit_file(); } /** * Search within a set of log files for a particular string. * * @param string $search The string to search for. * @param array $args Optional. Arguments for pagination of search results. * @param array $file_args Optional. Arguments to filter and sort the files that are returned. See get_files(). * @param bool $count_only Optional. True to return a total count of the matches. * * @return array|int|WP_Error When matches are found, each array item is an associative array that includes the * file ID, line number, and the matched string with HTML markup around the matched parts. */ public function search_within_files( string $search, array $args = array(), array $file_args = array(), bool $count_only = false ) { if ( '' === $search ) { return $count_only ? 0 : array(); } $search = esc_html( $search ); $args = wp_parse_args( $args, self::DEFAULTS_SEARCH_WITHIN_FILES ); $file_args = array_merge( $file_args, array( 'offset' => 0, 'per_page' => self::SEARCH_MAX_FILES, ) ); $cache_key = WC_Cache_Helper::get_prefixed_key( self::SEARCH_CACHE_KEY, self::CACHE_GROUP ); $query = wp_json_encode( array( $search, $args, $file_args ) ); $cache = wp_cache_get( $cache_key ); $is_cached = isset( $cache['query'], $cache['results'] ) && $query === $cache['query']; if ( true === $is_cached ) { $matched_lines = $cache['results']; } else { $files = $this->get_files( $file_args ); if ( is_wp_error( $files ) ) { return $files; } // Max string size * SEARCH_MAX_RESULTS = ~1MB largest possible cache entry. $max_string_size = 5 * KB_IN_BYTES; $matched_lines = array(); foreach ( $files as $file ) { $stream = $file->get_stream(); $line_number = 1; while ( ! feof( $stream ) ) { $line = fgets( $stream, $max_string_size ); if ( ! is_string( $line ) ) { continue; } $sanitized_line = esc_html( trim( $line ) ); if ( false !== stripos( $sanitized_line, $search ) ) { $matched_lines[] = array( 'file_id' => $file->get_file_id(), 'line_number' => $line_number, 'line' => $sanitized_line, ); } if ( count( $matched_lines ) >= self::SEARCH_MAX_RESULTS ) { $file->close_stream(); break 2; } if ( false !== strstr( $line, PHP_EOL ) ) { $line_number ++; } } $file->close_stream(); } $to_cache = array( 'query' => $query, 'results' => $matched_lines, ); wp_cache_set( $cache_key, $to_cache, self::CACHE_GROUP, DAY_IN_SECONDS ); } if ( true === $count_only ) { return count( $matched_lines ); } return array_slice( $matched_lines, $args['offset'], $args['per_page'] ); } /** * Invalidate the cache group related to log file data. * * @return bool True on successfully invalidating the cache. */ public function invalidate_cache(): bool { return WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP ); } } Logging/FileV2/FileListTable.php 0000644 00000017357 15073235522 0012436 0 ustar 00 <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2; use Automattic\WooCommerce\Internal\Admin\Logging\PageController; use WP_List_Table; /** * FileListTable class. */ class FileListTable extends WP_List_Table { /** * The user option key for saving the preferred number of files displayed per page. * * @const string */ public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_file_list_per_page'; /** * Instance of FileController. * * @var FileController */ private $file_controller; /** * Instance of PageController. * * @var PageController */ private $page_controller; /** * FileListTable class. * * @param FileController $file_controller Instance of FileController. * @param PageController $page_controller Instance of PageController. */ public function __construct( FileController $file_controller, PageController $page_controller ) { $this->file_controller = $file_controller; $this->page_controller = $page_controller; parent::__construct( array( 'singular' => 'log-file', 'plural' => 'log-files', 'ajax' => false, ) ); } /** * Render message when there are no items. * * @return void */ public function no_items(): void { esc_html_e( 'No log files found.', 'woocommerce' ); } /** * Retrieves the list of bulk actions available for this table. * * @return array */ protected function get_bulk_actions(): array { return array( 'export' => esc_html__( 'Download', 'woocommerce' ), 'delete' => esc_html__( 'Delete permanently', 'woocommerce' ), ); } /** * Get the existing log sources for the filter dropdown. * * @return array */ protected function get_sources_list(): array { $sources = $this->file_controller->get_file_sources(); if ( is_wp_error( $sources ) ) { return array(); } sort( $sources ); return $sources; } /** * Displays extra controls between bulk actions and pagination. * * @param string $which The location of the tablenav being rendered. 'top' or 'bottom'. * * @return void */ protected function extra_tablenav( $which ): void { $all_sources = $this->get_sources_list(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended $current_source = File::sanitize_source( wp_unslash( $_GET['source'] ?? '' ) ); ?> <div class="alignleft actions"> <?php if ( 'top' === $which ) : ?> <label for="filter-by-source" class="screen-reader-text"><?php esc_html_e( 'Filter by log source', 'woocommerce' ); ?></label> <select name="source" id="filter-by-source"> <option<?php selected( $current_source, '' ); ?> value=""><?php esc_html_e( 'All sources', 'woocommerce' ); ?></option> <?php foreach ( $all_sources as $source ) : ?> <option<?php selected( $current_source, $source ); ?> value="<?php echo esc_attr( $source ); ?>"> <?php echo esc_html( $source ); ?> </option> <?php endforeach; ?> </select> <?php submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'logs-filter-submit', ) ); ?> <?php endif; ?> </div> <?php } /** * Set up the column header info. * * @return void */ public function prepare_column_headers(): void { $this->_column_headers = array( $this->get_columns(), get_hidden_columns( $this->screen ), $this->get_sortable_columns(), $this->get_primary_column(), ); } /** * Prepares the list of items for displaying. * * @return void */ public function prepare_items(): void { $per_page = $this->get_items_per_page( self::PER_PAGE_USER_OPTION_KEY, $this->get_per_page_default() ); $defaults = array( 'per_page' => $per_page, 'offset' => ( $this->get_pagenum() - 1 ) * $per_page, ); $file_args = wp_parse_args( $this->page_controller->get_query_params( array( 'order', 'orderby', 'source' ) ), $defaults ); $total_items = $this->file_controller->get_files( $file_args, true ); if ( is_wp_error( $total_items ) ) { printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $total_items->get_error_message() ) ); return; } $total_pages = ceil( $total_items / $per_page ); $items = $this->file_controller->get_files( $file_args ); $this->items = $items; $this->set_pagination_args( array( 'per_page' => $per_page, 'total_items' => $total_items, 'total_pages' => $total_pages, ) ); } /** * Gets a list of columns. * * @return array */ public function get_columns(): array { $columns = array( 'cb' => '<input type="checkbox" />', 'source' => esc_html__( 'Source', 'woocommerce' ), 'created' => esc_html__( 'Date created', 'woocommerce' ), 'modified' => esc_html__( 'Date modified', 'woocommerce' ), 'size' => esc_html__( 'File size', 'woocommerce' ), ); return $columns; } /** * Gets a list of sortable columns. * * @return array */ protected function get_sortable_columns(): array { $sortable = array( 'source' => array( 'source' ), 'created' => array( 'created' ), 'modified' => array( 'modified', true ), 'size' => array( 'size' ), ); return $sortable; } /** * Render the checkbox column. * * @param File $item The current log file being rendered. * * @return string */ public function column_cb( $item ): string { ob_start(); ?> <input id="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>" type="checkbox" name="file_id[]" value="<?php echo esc_attr( $item->get_file_id() ); ?>" /> <label for="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>"> <span class="screen-reader-text"> <?php printf( // translators: 1. a date, 2. a slug-style name for a file. esc_html__( 'Select the %1$s log file for %2$s', 'woocommerce' ), esc_html( gmdate( get_option( 'date_format' ), $item->get_created_timestamp() ) ), esc_html( $item->get_source() ) ); ?> </span> </label> <?php return ob_get_clean(); } /** * Render the source column. * * @param File $item The current log file being rendered. * * @return string */ public function column_source( $item ): string { $log_file = $item->get_file_id(); $single_file_url = add_query_arg( array( 'view' => 'single_file', 'file_id' => $log_file, ), $this->page_controller->get_logs_tab_url() ); $rotation = ''; if ( ! is_null( $item->get_rotation() ) ) { $rotation = sprintf( ' – <span class="post-state">%d</span>', $item->get_rotation() ); } return sprintf( '<a class="row-title" href="%1$s">%2$s</a>%3$s', esc_url( $single_file_url ), esc_html( $item->get_source() ), $rotation ); } /** * Render the created column. * * @param File $item The current log file being rendered. * * @return string */ public function column_created( $item ): string { $timestamp = $item->get_created_timestamp(); return gmdate( 'Y-m-d', $timestamp ); } /** * Render the modified column. * * @param File $item The current log file being rendered. * * @return string */ public function column_modified( $item ): string { $timestamp = $item->get_modified_timestamp(); return gmdate( 'Y-m-d H:i:s', $timestamp ); } /** * Render the size column. * * @param File $item The current log file being rendered. * * @return string */ public function column_size( $item ): string { $size = $item->get_file_size(); return size_format( $size ); } /** * Helper to get the default value for the per_page arg. * * @return int */ public function get_per_page_default(): int { return $this->file_controller::DEFAULTS_GET_FILES['per_page']; } } Logging/FileV2/FileExporter.php 0000644 00000007341 15073235522 0012353 0 ustar 00 <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2; use WP_Error; use WP_Filesystem_Direct; /** * FileExport class. */ class FileExporter { /** * The number of bytes per read while streaming the file. * * @const int */ private const CHUNK_SIZE = 4 * KB_IN_BYTES; /** * The absolute path of the file. * * @var string */ private $path; /** * A name of the file to send to the browser rather than the filename part of the path. * * @var string */ private $alternate_filename; /** * Class FileExporter. * * @param string $path The absolute path of the file. * @param string $alternate_filename Optional. The name of the file to send to the browser rather than the filename * part of the path. */ public function __construct( string $path, string $alternate_filename = '' ) { global $wp_filesystem; if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) { WP_Filesystem(); } $this->path = $path; $this->alternate_filename = $alternate_filename; } /** * Configure PHP and stream the file to the browser. * * @return WP_Error|void Only returns something if there is an error. */ public function emit_file() { global $wp_filesystem; if ( ! $wp_filesystem->is_file( $this->path ) || ! $wp_filesystem->is_readable( $this->path ) ) { return new WP_Error( 'wc_logs_invalid_file', __( 'Could not access file.', 'woocommerce' ) ); } // These configuration tweaks are copied from WC_CSV_Exporter::send_headers(). // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged if ( function_exists( 'gc_enable' ) ) { gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound } if ( function_exists( 'apache_setenv' ) ) { @apache_setenv( 'no-gzip', '1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv } @ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky @ini_set( 'output_buffering', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky @ini_set( 'output_handler', '' ); // phpcs:ignore WordPress.PHP.IniSet.Risky ignore_user_abort( true ); wc_set_time_limit(); wc_nocache_headers(); // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged $this->send_headers(); $this->send_contents(); die; } /** * Send HTTP headers at the beginning of a file. * * Modeled on WC_CSV_Exporter::send_headers(). * * @return void */ private function send_headers(): void { header( 'Content-Type: text/plain; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=' . $this->get_filename() ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); } /** * Send the contents of the file. * * @return void */ private function send_contents(): void { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative. $stream = fopen( $this->path, 'rb' ); while ( is_resource( $stream ) && ! feof( $stream ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- No suitable alternative. $chunk = fread( $stream, self::CHUNK_SIZE ); if ( is_string( $chunk ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputting to file. echo $chunk; } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative. fclose( $stream ); } /** * Get the name of the file that will be sent to the browser. * * @return string */ private function get_filename(): string { if ( $this->alternate_filename ) { return $this->alternate_filename; } return basename( $this->path ); } } Logging/FileV2/SearchListTable.php 0000644 00000013173 15073235522 0012754 0 ustar 00 <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2; use Automattic\WooCommerce\Internal\Admin\Logging\PageController; use WP_List_Table; /** * SearchListTable class. */ class SearchListTable extends WP_List_Table { /** * The user option key for saving the preferred number of search results displayed per page. * * @const string */ public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_search_results_per_page'; /** * Instance of FileController. * * @var FileController */ private $file_controller; /** * Instance of PageController. * * @var PageController */ private $page_controller; /** * SearchListTable class. * * @param FileController $file_controller Instance of FileController. * @param PageController $page_controller Instance of PageController. */ public function __construct( FileController $file_controller, PageController $page_controller ) { $this->file_controller = $file_controller; $this->page_controller = $page_controller; parent::__construct( array( 'singular' => 'wc-logs-search-result', 'plural' => 'wc-logs-search-results', 'ajax' => false, ) ); } /** * Render message when there are no items. * * @return void */ public function no_items(): void { esc_html_e( 'No search results.', 'woocommerce' ); } /** * Set up the column header info. * * @return void */ public function prepare_column_headers(): void { $this->_column_headers = array( $this->get_columns(), array(), array(), $this->get_primary_column(), ); } /** * Prepares the list of items for displaying. * * @return void */ public function prepare_items(): void { $per_page = $this->get_items_per_page( self::PER_PAGE_USER_OPTION_KEY, $this->get_per_page_default() ); $args = array( 'per_page' => $per_page, 'offset' => ( $this->get_pagenum() - 1 ) * $per_page, ); $file_args = $this->page_controller->get_query_params( array( 'date_end', 'date_filter', 'date_start', 'order', 'orderby', 'search', 'source' ) ); $search = $file_args['search']; unset( $file_args['search'] ); $total_items = $this->file_controller->search_within_files( $search, $args, $file_args, true ); if ( is_wp_error( $total_items ) ) { printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $total_items->get_error_message() ) ); return; } if ( $total_items >= $this->file_controller::SEARCH_MAX_RESULTS ) { printf( '<div class="notice notice-info"><p>%s</p></div>', sprintf( // translators: %s is a number. esc_html__( 'The number of search results has reached the limit of %s. Try refining your search.', 'woocommerce' ), esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_RESULTS ) ) ) ); } $total_pages = ceil( $total_items / $per_page ); $results = $this->file_controller->search_within_files( $search, $args, $file_args ); $this->items = $results; $this->set_pagination_args( array( 'per_page' => $per_page, 'total_items' => $total_items, 'total_pages' => $total_pages, ) ); } /** * Gets a list of columns. * * @return array */ public function get_columns(): array { $columns = array( 'file_id' => esc_html__( 'File', 'woocommerce' ), 'line_number' => esc_html__( 'Line #', 'woocommerce' ), 'line' => esc_html__( 'Matched Line', 'woocommerce' ), ); return $columns; } /** * Render the file_id column. * * @param array $item The current search result being rendered. * * @return string */ public function column_file_id( array $item ): string { // Add a word break after the rotation number, if it exists. $file_id = preg_replace( '/\.([0-9])+\-/', '.\1<wbr>-', $item['file_id'] ); return wp_kses( $file_id, array( 'wbr' => array() ) ); } /** * Render the line_number column. * * @param array $item The current search result being rendered. * * @return string */ public function column_line_number( array $item ): string { $match_url = add_query_arg( array( 'view' => 'single_file', 'file_id' => $item['file_id'], ), $this->page_controller->get_logs_tab_url() . '#L' . absint( $item['line_number'] ) ); return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $match_url ), sprintf( // translators: %s is a line number in a file. esc_html__( 'Line %s', 'woocommerce' ), number_format_i18n( absint( $item['line_number'] ) ) ) ); } /** * Render the line column. * * @param array $item The current search result being rendered. * * @return string */ public function column_line( array $item ): string { $params = $this->page_controller->get_query_params( array( 'search' ) ); $line = $item['line']; // Highlight matches within the line. $pattern = preg_quote( $params['search'], '/' ); preg_match_all( "/$pattern/i", $line, $matches, PREG_OFFSET_CAPTURE ); if ( is_array( $matches[0] ) && count( $matches[0] ) >= 1 ) { $length_change = 0; foreach ( $matches[0] as $match ) { $replace = '<span class="search-match">' . $match[0] . '</span>'; $offset = $match[1] + $length_change; $orig_length = strlen( $match[0] ); $replace_length = strlen( $replace ); $line = substr_replace( $line, $replace, $offset, $orig_length ); $length_change += $replace_length - $orig_length; } } return wp_kses_post( $line ); } /** * Helper to get the default value for the per_page arg. * * @return int */ public function get_per_page_default(): int { return $this->file_controller::DEFAULTS_SEARCH_WITHIN_FILES['per_page']; } } Logging/FileV2/File.php 0000644 00000031755 15073235522 0010630 0 ustar 00 <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2; use Automattic\Jetpack\Constants; use WP_Filesystem_Direct; /** * File class. * * An object representation of a single log file. */ class File { /** * The absolute path of the file. * * @var string */ protected $path; /** * The source property of the file, derived from the filename. * * @var string */ protected $source = ''; /** * The 0-based increment of the file, if it has been rotated. Derived from the filename. Can only be 0-9. * * @var int|null */ protected $rotation; /** * The date the file was created, as a Unix timestamp, derived from the filename. * * @var int */ protected $created = 0; /** * The hash property of the file, derived from the filename. * * @var string */ protected $hash = ''; /** * The file's resource handle when it is open. * * @var resource */ protected $stream; /** * Class File * * @param string $path The absolute path of the file. */ public function __construct( $path ) { require_once ABSPATH . 'wp-admin/includes/file.php'; global $wp_filesystem; if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) { WP_Filesystem(); } $this->path = $path; $this->ingest_path(); } /** * Make sure open streams are closed. */ public function __destruct() { if ( is_resource( $this->stream ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative. fclose( $this->stream ); } } /** * Parse a path to a log file to determine if it uses the standard filename structure and various properties. * * This makes assumptions about the structure of the log file's name. Using `-` to separate the name into segments, * if there are at least 5 segments, it assumes that the last segment is the hash, and the three segments before * that make up the date when the file was created in YYYY-MM-DD format. Any segments left after that are the * "source" that generated the log entries. If the filename doesn't have enough segments, it falls back to the * source and the hash both being the entire filename, and using the inode change time as the creation date. * * Example: * my-custom-plugin.2-2025-01-01-a1b2c3d4e5f.log * | | | | * 'my-custom-plugin' | '2025-01-01' | * (source) | (created) | * '2' 'a1b2c3d4e5f' * (rotation) (hash) * * @param string $path The full path of the log file. * * @return array { * @type string $dirname The directory structure containing the file. See pathinfo(). * @type string $basename The filename with extension. See pathinfo(). * @type string $extension The file extension. See pathinfo(). * @type string $filename The filename without extension. See pathinfo(). * @type string $source The source of the log entries contained in the file. * @type int|null $rotation The 0-based incremental rotation marker, if the file has been rotated. * Should only be a single digit. * @type int $created The date the file was created, as a Unix timestamp. * @type string $hash The hash suffix of the filename that protects from direct access. * @type string $file_id The public ID of the log file (filename without the hash). * } */ public static function parse_path( string $path ): array { $defaults = array( 'dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '', 'source' => '', 'rotation' => null, 'created' => 0, 'hash' => '', 'file_id' => '', ); $parsed = array_merge( $defaults, pathinfo( $path ) ); $segments = explode( '-', $parsed['filename'] ); $timestamp = strtotime( implode( '-', array_slice( $segments, -4, 3 ) ) ); if ( count( $segments ) >= 5 && false !== $timestamp ) { $parsed['source'] = implode( '-', array_slice( $segments, 0, -4 ) ); $parsed['created'] = $timestamp; $parsed['hash'] = array_slice( $segments, -1 )[0]; } else { $parsed['source'] = implode( '-', $segments ); } $rotation_marker = strrpos( $parsed['source'], '.', -1 ); if ( false !== $rotation_marker ) { $rotation = substr( $parsed['source'], -1 ); if ( is_numeric( $rotation ) ) { $parsed['rotation'] = intval( $rotation ); } $parsed['source'] = substr( $parsed['source'], 0, $rotation_marker ); } $parsed['file_id'] = static::generate_file_id( $parsed['source'], $parsed['rotation'], $parsed['created'] ); return $parsed; } /** * Generate a public ID for a log file based on its properties. * * The file ID is the basename of the file without the hash part. It allows us to identify a file without revealing * its full name in the filesystem, so that it's difficult to access the file directly with an HTTP request. * * @param string $source The source of the log entries contained in the file. * @param int|null $rotation Optional. The 0-based incremental rotation marker, if the file has been rotated. * Should only be a single digit. * @param int $created Optional. The date the file was created, as a Unix timestamp. * * @return string */ public static function generate_file_id( string $source, ?int $rotation = null, int $created = 0 ): string { $file_id = static::sanitize_source( $source ); if ( ! is_null( $rotation ) ) { $file_id .= '.' . $rotation; } if ( $created > 0 ) { $file_id .= '-' . gmdate( 'Y-m-d', $created ); } return $file_id; } /** * Generate a hash to use as the suffix on a log filename. * * @param string $file_id A file ID (file basename without the hash). * * @return string */ public static function generate_hash( string $file_id ): string { $key = Constants::get_constant( 'AUTH_SALT' ) ?? 'wc-logs'; return hash_hmac( 'md5', $file_id, $key ); } /** * Sanitize the source property of a log file. * * @param string $source The source of the log entries contained in the file. * * @return string */ public static function sanitize_source( string $source ): string { return sanitize_file_name( $source ); } /** * Parse the log file path and assign various properties to this class instance. * * @return void */ protected function ingest_path(): void { $parsed_path = static::parse_path( $this->path ); $this->source = $parsed_path['source']; $this->rotation = $parsed_path['rotation']; $this->created = $parsed_path['created']; $this->hash = $parsed_path['hash']; } /** * Check if the filename structure is in the expected format. * * @see parse_path(). * * @return bool */ public function has_standard_filename(): bool { return ! ! $this->get_hash(); } /** * Check if the file represented by the class instance is a file and is readable. * * @global WP_Filesystem_Direct $wp_filesystem * * @return bool */ public function is_readable(): bool { global $wp_filesystem; return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_readable( $this->path ); } /** * Check if the file represented by the class instance is a file and is writable. * * @global WP_Filesystem_Direct $wp_filesystem * * @return bool */ public function is_writable(): bool { global $wp_filesystem; return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_writable( $this->path ); } /** * Open a read-only stream for this file. * * @return resource|false */ public function get_stream() { if ( ! $this->is_readable() ) { return false; } if ( ! is_resource( $this->stream ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative. $this->stream = fopen( $this->path, 'rb' ); } return $this->stream; } /** * Close the stream for this file. * * The stream will also close automatically when the class instance destructs, but this can be useful for * avoiding having a large number of streams open simultaneously. * * @return bool */ public function close_stream(): bool { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative. return fclose( $this->stream ); } /** * Get the full absolute path of the file. * * @return string */ public function get_path(): string { return $this->path; } /** * Get the name of the file, with extension, but without full path. * * @return string */ public function get_basename(): string { return basename( $this->path ); } /** * Get the file's source property. * * @return string */ public function get_source(): string { return $this->source; } /** * Get the file's rotation property. * * @return int|null */ public function get_rotation(): ?int { return $this->rotation; } /** * Get the file's hash property. * * @return string */ public function get_hash(): string { return $this->hash; } /** * Get the file's public ID. * * @return string */ public function get_file_id(): string { $created = 0; if ( $this->has_standard_filename() ) { $created = $this->get_created_timestamp(); } $file_id = static::generate_file_id( $this->get_source(), $this->get_rotation(), $created ); return $file_id; } /** * Get the file's created property. * * @return int */ public function get_created_timestamp(): int { if ( ! $this->created && $this->is_readable() ) { $this->created = filectime( $this->path ); } return $this->created; } /** * Get the time of the last modification of the file, as a Unix timestamp. Or false if the file isn't readable. * * @global WP_Filesystem_Direct $wp_filesystem * * @return int|false */ public function get_modified_timestamp() { global $wp_filesystem; return $wp_filesystem->mtime( $this->path ); } /** * Get the size of the file in bytes. Or false if the file isn't readable. * * @global WP_Filesystem_Direct $wp_filesystem * * @return int|false */ public function get_file_size() { global $wp_filesystem; if ( ! $wp_filesystem->is_readable( $this->path ) ) { return false; } return $wp_filesystem->size( $this->path ); } /** * Create and set permissions on the file. * * @return bool */ protected function create(): bool { global $wp_filesystem; $created = $wp_filesystem->touch( $this->path ); $modded = $wp_filesystem->chmod( $this->path ); return $created && $modded; } /** * Write content to the file, appending it to the end. * * @param string $text The content to add to the file. * * @return bool */ public function write( string $text ): bool { if ( ! $this->is_writable() ) { $created = $this->create(); if ( ! $created || ! $this->is_writable() ) { return false; } } // Ensure content ends with a line ending. $eol_pos = strrpos( $text, PHP_EOL ); if ( false === $eol_pos || strlen( $text ) !== $eol_pos + 1 ) { $text .= PHP_EOL; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative. $resource = fopen( $this->path, 'ab' ); mbstring_binary_safe_encoding(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- No suitable alternative. $bytes_written = fwrite( $resource, $text ); reset_mbstring_encoding(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative. fclose( $resource ); if ( strlen( $text ) !== $bytes_written ) { return false; } return true; } /** * Rename this file with an incremented rotation number. * * @return bool True if the file was successfully rotated. */ public function rotate(): bool { if ( ! $this->is_writable() ) { return false; } global $wp_filesystem; $created = 0; if ( $this->has_standard_filename() ) { $created = $this->get_created_timestamp(); } if ( is_null( $this->get_rotation() ) ) { $new_rotation = 0; } else { $new_rotation = $this->get_rotation() + 1; } $new_file_id = static::generate_file_id( $this->get_source(), $new_rotation, $created ); $search = array( $this->get_file_id() ); $replace = array( $new_file_id ); if ( $this->has_standard_filename() ) { $search[] = $this->get_hash(); $replace[] = static::generate_hash( $new_file_id ); } $old_filename = $this->get_basename(); $new_filename = str_replace( $search, $replace, $old_filename ); $new_path = str_replace( $old_filename, $new_filename, $this->path ); $moved = $wp_filesystem->move( $this->path, $new_path, true ); if ( ! $moved ) { return false; } $this->path = $new_path; $this->ingest_path(); return $this->is_readable(); } /** * Delete the file from the filesystem. * * @global WP_Filesystem_Direct $wp_filesystem * * @return bool True on success, false on failure. */ public function delete(): bool { global $wp_filesystem; return $wp_filesystem->delete( $this->path, false, 'f' ); } } Logging/LogHandlerFileV2.php 0000644 00000014414 15073235522 0011702 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\Logging; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController; use Automattic\WooCommerce\Proxies\LegacyProxy; use WC_Log_Handler; /** * LogHandlerFileV2 class. */ class LogHandlerFileV2 extends WC_Log_Handler { /** * Instance of the FileController class. * * @var FileController */ private $file_controller; /** * LogHandlerFileV2 class. */ public function __construct() { $this->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 <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Admin\Logging; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2; use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable }; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use WC_Admin_Status; use WC_Log_Handler_File, WC_Log_Handler_DB; use WC_Log_Levels; use WP_List_Table; /** * PageController class. */ class PageController { use AccessiblePrivateMethods; /** * Instance of FileController. * * @var FileController */ private $file_controller; /** * Instance of FileListTable or SearchListTable. * * @var FileListTable|SearchListTable */ private $list_table; /** * Initialize dependencies. * * @internal * * @param FileController $file_controller Instance of FileController. * * @return void */ final public function init( FileController $file_controller ): void { $this->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(); ?> <header id="logs-header" class="wc-logs-header"> <h2> <?php esc_html_e( 'Browse log files', 'woocommerce' ); ?> </h2> <?php $this->render_search_field(); ?> </header> <form id="logs-list-table-form" method="get"> <input type="hidden" name="page" value="wc-status" /> <input type="hidden" name="tab" value="logs" /> <?php foreach ( $params as $key => $value ) : ?> <?php if ( $value !== $defaults[ $key ] ) : ?> <input type="hidden" name="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $value ); ?>" /> <?php endif; ?> <?php endforeach; ?> <?php $list_table->display(); ?> </form> <?php } /** * Render the single file view. * * @return void */ private function render_single_file_view(): void { $params = $this->get_query_params( array( 'file_id', 'view' ) ); $file = $this->file_controller->get_file_by_id( $params['file_id'] ); if ( is_wp_error( $file ) ) { ?> <div class="notice notice-error notice-inline"> <?php echo wp_kses_post( wpautop( $file->get_error_message() ) ); ?> <?php printf( '<p><a href="%1$s">%2$s</a></p>', esc_url( $this->get_logs_tab_url() ), esc_html__( 'Return to the file list.', 'woocommerce' ) ); ?> </div> <?php return; } $rotations = $this->file_controller->get_file_rotations( $file->get_file_id() ); $rotation_url_base = add_query_arg( 'view', 'single_file', $this->get_logs_tab_url() ); $download_url = add_query_arg( array( 'action' => 'export', 'file_id' => array( $file->get_file_id() ), ), wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' ) ); $delete_url = add_query_arg( array( 'action' => 'delete', 'file_id' => array( $file->get_file_id() ), ), wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' ) ); $delete_confirmation_js = sprintf( "return window.confirm( '%s' )", esc_js( __( 'Delete this log file permanently?', 'woocommerce' ) ) ); $stream = $file->get_stream(); $line_number = 1; ?> <header id="logs-header" class="wc-logs-header"> <h2> <?php printf( // translators: %s is the name of a log file. esc_html__( 'Viewing log file %s', 'woocommerce' ), sprintf( '<span class="file-id">%s</span>', esc_html( $file->get_file_id() ) ) ); ?> </h2> <?php if ( count( $rotations ) > 1 ) : ?> <nav class="wc-logs-single-file-rotations"> <h3><?php esc_html_e( 'File rotations:', 'woocommerce' ); ?></h3> <ul class="wc-logs-rotation-links"> <?php if ( isset( $rotations['current'] ) ) : ?> <?php printf( '<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>', esc_url( add_query_arg( 'file_id', $rotations['current']->get_file_id(), $rotation_url_base ) ), $file->get_file_id() === $rotations['current']->get_file_id() ? 'primary' : 'secondary', esc_html__( 'Current', 'woocommerce' ) ); unset( $rotations['current'] ); ?> <?php endif; ?> <?php foreach ( $rotations as $rotation ) : ?> <?php printf( '<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>', esc_url( add_query_arg( 'file_id', $rotation->get_file_id(), $rotation_url_base ) ), $file->get_file_id() === $rotation->get_file_id() ? 'primary' : 'secondary', absint( $rotation->get_rotation() ) ); ?> <?php endforeach; ?> </ul> </nav> <?php endif; ?> <div class="wc-logs-single-file-actions"> <?php // Download button. printf( '<a href="%1$s" class="button button-secondary">%2$s</a>', esc_url( $download_url ), esc_html__( 'Download', 'woocommerce' ) ); ?> <?php // Delete button. printf( '<a href="%1$s" class="button button-secondary" onclick="%2$s">%3$s</a>', esc_url( $delete_url ), esc_attr( $delete_confirmation_js ), esc_html__( 'Delete permanently', 'woocommerce' ) ); ?> </div> </header> <section id="logs-entries" class="wc-logs-entries"> <?php while ( ! feof( $stream ) ) : ?> <?php $line = fgets( $stream ); if ( is_string( $line ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_line does the escaping. echo $this->format_line( $line, $line_number ); $line_number ++; } ?> <?php endwhile; ?> </section> <?php } /** * Render the search results view. * * @return void */ private function render_search_results_view(): void { $params = $this->get_query_params( array( 'order', 'orderby', 'search', 'source', 'view' ) ); $list_table = $this->get_list_table( $params['view'] ); $list_table->prepare_items(); ?> <header id="logs-header" class="wc-logs-header"> <h2><?php esc_html_e( 'Search results', 'woocommerce' ); ?></h2> <?php $this->render_search_field(); ?> </header> <?php $list_table->display(); ?> <?php } /** * Get the default values for URL query params for FileV2 views. * * @return string[] */ public function get_query_param_defaults(): array { return array( 'file_id' => '', 'order' => $this->file_controller::DEFAULTS_GET_FILES['order'], 'orderby' => $this->file_controller::DEFAULTS_GET_FILES['orderby'], 'search' => '', 'source' => $this->file_controller::DEFAULTS_GET_FILES['source'], 'view' => 'list_files', ); } /** * Get and validate URL query params for FileV2 views. * * @param array $param_keys Optional. The names of the params you want to get. * * @return array */ public function get_query_params( array $param_keys = array() ): array { $defaults = $this->get_query_param_defaults(); $params = filter_input_array( INPUT_GET, array( 'file_id' => array( 'filter' => FILTER_CALLBACK, 'options' => function( $file_id ) { return sanitize_file_name( wp_unslash( $file_id ) ); }, ), 'order' => array( 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array( 'regexp' => '/^(asc|desc)$/i', 'default' => $defaults['order'], ), ), 'orderby' => array( 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array( 'regexp' => '/^(created|modified|source|size)$/', 'default' => $defaults['orderby'], ), ), 'search' => array( 'filter' => FILTER_CALLBACK, 'options' => function( $search ) { return esc_html( wp_unslash( $search ) ); }, ), 'source' => array( 'filter' => FILTER_CALLBACK, 'options' => function( $source ) { return File::sanitize_source( wp_unslash( $source ) ); }, ), 'view' => array( 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array( 'regexp' => '/^(list_files|single_file|search_results)$/', 'default' => $defaults['view'], ), ), ), false ); $params = wp_parse_args( $params, $defaults ); if ( count( $param_keys ) > 0 ) { $params = array_intersect_key( $params, array_flip( $param_keys ) ); } return $params; } /** * Get and cache an instance of the list table. * * @param string $view The current view, which determines which list table class to get. * * @return FileListTable|SearchListTable */ private function get_list_table( string $view ) { if ( $this->list_table instanceof WP_List_Table ) { return $this->list_table; } switch ( $view ) { case 'list_files': $this->list_table = new FileListTable( $this->file_controller, $this ); break; case 'search_results': $this->list_table = new SearchListTable( $this->file_controller, $this ); break; } return $this->list_table; } /** * Register screen options for the logging views. * * @return void */ private function setup_screen_options(): void { $params = $this->get_query_params( array( 'view' ) ); $handler = $this->get_default_handler(); $list_table = null; switch ( $handler ) { case LogHandlerFileV2::class: if ( in_array( $params['view'], array( 'list_files', 'search_results' ), true ) ) { $list_table = $this->get_list_table( $params['view'] ); } break; case 'WC_Log_Handler_DB': $list_table = WC_Admin_Status::get_db_log_list_table(); break; } if ( $list_table instanceof WP_List_Table ) { // Ensure list table columns are initialized early enough to enable column hiding, if available. $list_table->prepare_column_headers(); add_screen_option( 'per_page', array( 'default' => $list_table->get_per_page_default(), 'option' => $list_table::PER_PAGE_USER_OPTION_KEY, ) ); } } /** * Process bulk actions initiated from the log file list table. * * @return void */ private function handle_list_table_bulk_actions(): void { // Bail if we're not using the file handler. if ( LogHandlerFileV2::class !== $this->get_default_handler() ) { return; } $params = $this->get_query_params( array( 'file_id', 'view' ) ); // Bail if this is not the list table view. if ( 'list_files' !== $params['view'] ) { return; } $action = $this->get_list_table( $params['view'] )->current_action(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : $this->get_logs_tab_url(); if ( $action ) { check_admin_referer( 'bulk-log-files' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to manage log files.', 'woocommerce' ) ); } $sendback = remove_query_arg( array( 'deleted' ), wp_get_referer() ); // Multiple file_id[] params will be filtered separately, but assigned to $files as an array. $file_ids = $params['file_id']; if ( ! is_array( $file_ids ) || count( $file_ids ) < 1 ) { wp_safe_redirect( $sendback ); exit; } switch ( $action ) { case 'export': if ( 1 === count( $file_ids ) ) { $export_error = $this->file_controller->export_single_file( reset( $file_ids ) ); } else { $export_error = $this->file_controller->export_multiple_files( $file_ids ); } if ( is_wp_error( $export_error ) ) { wp_die( wp_kses_post( $export_error ) ); } break; case 'delete': $deleted = $this->file_controller->delete_files( $file_ids ); $sendback = add_query_arg( 'deleted', $deleted, $sendback ); /** * If the delete action was triggered on the single file view, don't redirect back there * since the file doesn't exist anymore. */ $sendback = remove_query_arg( array( 'view', 'file_id' ), $sendback ); break; } $sendback = remove_query_arg( array( 'action', 'action2' ), $sendback ); wp_safe_redirect( $sendback ); exit; } elseif ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { $removable_args = array( '_wp_http_referer', '_wpnonce', 'action', 'action2', 'filter_action' ); wp_safe_redirect( remove_query_arg( $removable_args, $request_uri ) ); exit; } $deleted = filter_input( INPUT_GET, 'deleted', FILTER_VALIDATE_INT ); if ( is_numeric( $deleted ) ) { add_action( 'admin_notices', function() use ( $deleted ) { ?> <div class="notice notice-info is-dismissible"> <p> <?php printf( // translators: %s is a number of files. esc_html( _n( '%s log file deleted.', '%s log files deleted.', $deleted, 'woocommerce' ) ), esc_html( number_format_i18n( $deleted ) ) ); ?> </p> </div> <?php } ); } } /** * Format a log file line. * * @param string $line The unformatted log file line. * @param int $line_number The line number. * * @return string */ private function format_line( string $line, int $line_number ): string { $severity_levels = WC_Log_Levels::get_all_severity_levels(); $classes = array( 'line' ); $line = esc_html( trim( $line ) ); if ( empty( $line ) ) { $line = ' '; } $segments = explode( ' ', $line, 3 ); $has_timestamp = false; $has_level = false; if ( isset( $segments[0] ) && false !== strtotime( $segments[0] ) ) { $classes[] = 'log-entry'; $segments[0] = sprintf( '<span class="log-timestamp">%s</span>', $segments[0] ); $has_timestamp = true; } if ( isset( $segments[1] ) && in_array( strtolower( $segments[1] ), $severity_levels, true ) ) { $segments[1] = sprintf( '<span class="%1$s">%2$s</span>', esc_attr( 'log-level log-level--' . strtolower( $segments[1] ) ), esc_html( $segments[1] ) ); $has_level = true; } if ( isset( $segments[2] ) && $has_timestamp && $has_level ) { $message_chunks = explode( 'CONTEXT:', $segments[2], 2 ); if ( isset( $message_chunks[1] ) ) { try { $maybe_json = stripslashes( html_entity_decode( trim( $message_chunks[1] ) ) ); $context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR ); $message_chunks[1] = sprintf( '<details><summary>%1$s</summary><pre>%2$s</pre></details>', esc_html__( 'Additional context', 'woocommerce' ), wp_json_encode( $context, JSON_PRETTY_PRINT ) ); $segments[2] = implode( ' ', $message_chunks ); $classes[] = 'has-context'; } catch ( \JsonException $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // It's not valid JSON so don't do anything with it. } } } if ( count( $segments ) > 1 ) { $line = implode( ' ', $segments ); } $classes = implode( ' ', $classes ); return sprintf( '<span id="L%1$d" class="%2$s">%3$s%4$s</span>', absint( $line_number ), esc_attr( $classes ), sprintf( '<a href="#L%1$d" class="line-anchor"></a>', absint( $line_number ) ), sprintf( '<span class="line-content">%s</span>', wp_kses_post( $line ) ) ); } /** * Render a form for searching within log files. * * @return void */ private function render_search_field(): void { $params = $this->get_query_params( array( 'date_end', 'date_filter', 'date_start', 'search', 'source' ) ); $defaults = $this->get_query_param_defaults(); $file_count = $this->file_controller->get_files( $params, true ); if ( $file_count > 0 ) { ?> <form id="logs-search" class="wc-logs-search" method="get"> <fieldset class="wc-logs-search-fieldset"> <input type="hidden" name="page" value="wc-status" /> <input type="hidden" name="tab" value="logs" /> <input type="hidden" name="view" value="search_results" /> <?php foreach ( $params as $key => $value ) : ?> <?php if ( $value !== $defaults[ $key ] ) : ?> <input type="hidden" name="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $value ); ?>" /> <?php endif; ?> <?php endforeach; ?> <label for="logs-search-field"> <?php esc_html_e( 'Search within these files', 'woocommerce' ); ?> <input id="logs-search-field" class="wc-logs-search-field" type="text" name="search" value="<?php echo esc_attr( $params['search'] ); ?>" /> </label> <?php submit_button( __( 'Search', 'woocommerce' ), 'secondary', null, false ); ?> </fieldset> <?php if ( $file_count >= $this->file_controller::SEARCH_MAX_FILES ) : ?> <div class="wc-logs-search-notice"> <?php printf( // translators: %s is a number. esc_html__( '⚠️ Only %s files can be searched at one time. Try filtering the file list before searching.', 'woocommerce' ), esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_FILES ) ) ); ?> </div> <?php endif; ?> </form> <?php } } } WCAdminUser.php 0000644 00000007745 15073235522 0007417 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin; /** * WCAdminUser Class. */ class WCAdminUser { /** * Class instance. * * @var WCAdminUser instance */ protected static $instance = null; /** * Constructor. */ public function __construct() { add_action( 'rest_api_init', array( $this, 'register_user_data' ) ); } /** * Get class instance. * * @return object Instance. */ public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Registers WooCommerce specific user data to the WordPress user API. */ public function register_user_data() { register_rest_field( 'user', 'is_super_admin', array( 'get_callback' => function() { return is_super_admin(); }, 'schema' => null, ) ); register_rest_field( 'user', 'woocommerce_meta', array( 'get_callback' => array( $this, 'get_user_data_values' ), 'update_callback' => array( $this, 'update_user_data_values' ), 'schema' => null, ) ); } /** * For all the registered user data fields ( Loader::get_user_data_fields ), fetch the data * for returning via the REST API. * * @param WP_User $user Current user. */ public function get_user_data_values( $user ) { $values = array(); foreach ( $this->get_user_data_fields() as $field ) { $values[ $field ] = self::get_user_data_field( $user['id'], $field ); } return $values; } /** * For all the registered user data fields ( Loader::get_user_data_fields ), update the data * for the REST API. * * @param array $values The new values for the meta. * @param WP_User $user The current user. * @param string $field_id The field id for the user meta. */ public function update_user_data_values( $values, $user, $field_id ) { if ( empty( $values ) || ! is_array( $values ) || 'woocommerce_meta' !== $field_id ) { return; } $fields = $this->get_user_data_fields(); $updates = array(); foreach ( $values as $field => $value ) { if ( in_array( $field, $fields, true ) ) { $updates[ $field ] = $value; self::update_user_data_field( $user->ID, $field, $value ); } } return $updates; } /** * We store some WooCommerce specific user meta attached to users endpoint, * so that we can track certain preferences or values such as the inbox activity panel last open time. * Additional fields can be added in the function below, and then used via wc-admin's currentUser data. * * @return array Fields to expose over the WP user endpoint. */ public function get_user_data_fields() { /** * Filter user data fields exposed over the WordPress user endpoint. * * @since 4.0.0 * @param array $fields Array of fields to expose over the WP user endpoint. */ return apply_filters( 'woocommerce_admin_get_user_data_fields', array( 'variable_product_tour_shown' ) ); } /** * Helper to update user data fields. * * @param int $user_id User ID. * @param string $field Field name. * @param mixed $value Field value. */ public static function update_user_data_field( $user_id, $field, $value ) { update_user_meta( $user_id, 'woocommerce_admin_' . $field, $value ); } /** * Helper to retrieve user data fields. * * Migrates old key prefixes as well. * * @param int $user_id User ID. * @param string $field Field name. * @return mixed The user field value. */ public static function get_user_data_field( $user_id, $field ) { $meta_value = get_user_meta( $user_id, 'woocommerce_admin_' . $field, true ); // Migrate old meta values (prefix changed from `wc_admin_` to `woocommerce_admin_`). if ( '' === $meta_value ) { $old_meta_value = get_user_meta( $user_id, 'wc_admin_' . $field, true ); if ( '' !== $old_meta_value ) { self::update_user_data_field( $user_id, $field, $old_meta_value ); delete_user_meta( $user_id, 'wc_admin_' . $field ); $meta_value = $old_meta_value; } } return $meta_value; } } Marketing/MarketingSpecs.php 0000644 00000012544 15073235522 0012127 0 ustar 00 <?php /** * Marketing Specs Handler * * Fetches the specifications for the marketing feature from Woo.com API. */ namespace Automattic\WooCommerce\Internal\Admin\Marketing; /** * Marketing Specifications Class. * * @internal * @since x.x.x */ class MarketingSpecs { /** * Name of recommended plugins transient. * * @var string */ const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins'; /** * Name of knowledge base post transient. * * @var string */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; /** * Slug of the category specifying marketing extensions on the Woo.com store. * * @var string */ const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; /** * Slug of the subcategory specifying marketing channels on the Woo.com store. * * @var string */ const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; /** * Load recommended plugins from Woo.com * * @return array */ public function get_recommended_plugins(): array { $plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT ); if ( false === $plugins ) { $request = wp_remote_get( 'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json', array( 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); $plugins = []; if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { $plugins = json_decode( $request['body'], true ); } set_transient( self::RECOMMENDED_PLUGINS_TRANSIENT, $plugins, // Expire transient in 15 minutes if remote get failed. // Cache an empty result to avoid repeated failed requests. empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS ); } return array_values( $plugins ); } /** * Return only the recommended marketing channels from Woo.com. * * @return array */ public function get_recommended_marketing_channels(): array { return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); } /** * Return all recommended marketing extensions EXCEPT the marketing channels from Woo.com. * * @return array */ public function get_recommended_marketing_extensions_excluding_channels(): array { return array_filter( $this->get_recommended_plugins(), function ( array $plugin_data ) { return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); } ); } /** * Returns whether a plugin is a marketing extension. * * @param array $plugin_data The plugin properties returned by the API. * * @return bool */ protected function is_marketing_plugin( array $plugin_data ): bool { $categories = $plugin_data['categories'] ?? []; return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); } /** * Returns whether a plugin is a marketing channel. * * @param array $plugin_data The plugin properties returned by the API. * * @return bool */ protected function is_marketing_channel_plugin( array $plugin_data ): bool { if ( ! $this->is_marketing_plugin( $plugin_data ) ) { return false; } $subcategories = $plugin_data['subcategories'] ?? []; foreach ( $subcategories as $subcategory ) { if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { return true; } } return false; } /** * Load knowledge base posts from Woo.com * * @param string|null $topic The topic of marketing knowledgebase to retrieve. * @return array */ public function get_knowledge_base_posts( ?string $topic ): array { // Default to the marketing topic (if no topic is set on the kb component). if ( empty( $topic ) ) { $topic = 'marketing'; } $kb_transient = self::KNOWLEDGE_BASE_TRANSIENT . '_' . strtolower( $topic ); $posts = get_transient( $kb_transient ); if ( false === $posts ) { $request_url = add_query_arg( array( 'page' => 1, 'per_page' => 8, '_embed' => 1, ), 'https://woocommerce.com/wp-json/wccom/marketing-knowledgebase/v1/posts/' . $topic ); $request = wp_remote_get( $request_url, array( 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); $posts = []; if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { $raw_posts = json_decode( $request['body'], true ); foreach ( $raw_posts as $raw_post ) { $post = [ 'title' => html_entity_decode( $raw_post['title']['rendered'] ), 'date' => $raw_post['date_gmt'], 'link' => $raw_post['link'], 'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '', 'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '', ]; $featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? []; if ( count( $featured_media ) > 0 ) { $image = current( $featured_media ); $post['image'] = add_query_arg( array( 'resize' => '650,340', 'crop' => 1, ), $image['source_url'] ); } $posts[] = $post; } } set_transient( $kb_transient, $posts, // Expire transient in 15 minutes if remote get failed. empty( $posts ) ? 900 : DAY_IN_SECONDS ); } return $posts; } } SettingsNavigationFeature.php 0000644 00000010521 15073235522 0012414 0 ustar 00 <?php /** * WooCommerce Settings. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\PageController; /** * Contains backend logic for the Settings feature. */ class SettingsNavigationFeature { /** * Option name used to toggle this feature. */ const TOGGLE_OPTION_NAME = 'woocommerce_settings_enabled'; /** * Class instance. * * @var Settings instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { if ( ! is_admin() ) { return; } add_filter( 'woocommerce_settings_features', array( $this, 'add_feature_toggle' ) ); if ( 'yes' !== get_option( 'woocommerce_settings_enabled', 'no' ) ) { return; } add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_component_settings' ) ); // Run this after the original WooCommerce settings have been added. add_action( 'admin_menu', array( $this, 'register_pages' ), 60 ); add_action( 'init', array( $this, 'redirect_core_settings_pages' ) ); } /** * Add the necessary data to initially load the WooCommerce Settings pages. * * @param array $settings Array of component settings. * @return array Array of component settings. */ public static function add_component_settings( $settings ) { if ( ! is_admin() ) { return $settings; } $setting_pages = \WC_Admin_Settings::get_settings_pages(); $pages = array(); foreach ( $setting_pages as $setting_page ) { $pages = $setting_page->add_settings_page( $pages ); } $settings['settingsPages'] = $pages; return $settings; } /** * Add the feature toggle to the features settings. * * @param array $features Feature sections. * @return array */ public static function add_feature_toggle( $features ) { $features[] = array( 'title' => __( 'Settings', 'woocommerce' ), 'desc' => __( 'Adds the new WooCommerce settings UI.', 'woocommerce' ), 'id' => 'woocommerce_settings_enabled', 'type' => 'checkbox', ); return $features; } /** * Registers settings pages. */ public function register_pages() { $controller = PageController::get_instance(); $setting_pages = \WC_Admin_Settings::get_settings_pages(); $settings = array(); foreach ( $setting_pages as $setting_page ) { $settings = $setting_page->add_settings_page( $settings ); } $order = 0; foreach ( $settings as $key => $setting ) { $order += 10; $settings_page = array( 'parent' => 'woocommerce-settings', 'title' => $setting, 'id' => 'settings-' . $key, 'path' => "/settings/$key", 'nav_args' => array( 'capability' => 'manage_woocommerce', 'order' => $order, 'parent' => 'woocommerce-settings', ), ); // Replace the old menu with the first settings item. if ( 10 === $order ) { $this->replace_settings_page( $settings_page ); } $controller->register_page( $settings_page ); } } /** * Replace the Settings page in the original WooCommerce menu. * * @param array $page Page used to replace the original. */ protected function replace_settings_page( $page ) { global $submenu; // Check if WooCommerce parent menu has been registered. if ( ! isset( $submenu['woocommerce'] ) ) { return; } foreach ( $submenu['woocommerce'] as &$item ) { // The "slug" (aka the path) is the third item in the array. if ( 0 === strpos( $item[2], 'wc-settings' ) ) { $item[2] = wc_admin_url( "&path={$page['path']}" ); } } } /** * Redirect the old settings page URLs to the new ones. */ public function redirect_core_settings_pages() { /* phpcs:disable WordPress.Security.NonceVerification */ if ( ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { return; } if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $setting_pages = \WC_Admin_Settings::get_settings_pages(); $default_setting = isset( $setting_pages[0] ) ? $setting_pages[0]->get_id() : ''; $setting = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : $default_setting; /* phpcs:enable */ wp_safe_redirect( wc_admin_url( "&path=/settings/$setting" ) ); exit; } } BlockTemplateRegistry/BlockTemplatesController.php 0000644 00000002675 15073235522 0016527 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry; /** * Block template controller. */ class BlockTemplatesController { /** * Block template registry * * @var BlockTemplateRegistry */ private $block_template_registry; /** * Block template transformer. * * @var TemplateTransformer */ private $template_transformer; /** * Init. */ public function init( $block_template_registry, $template_transformer ) { $this->block_template_registry = $block_template_registry; $this->template_transformer = $template_transformer; add_action( 'rest_api_init', array( $this, 'register_templates' ) ); } /** * Register templates in the blocks endpoint. */ public function register_templates() { $templates = $this->block_template_registry->get_all_registered(); foreach ( $templates as $template ) { add_filter( 'pre_get_block_templates', function( $query_result, $query, $template_type ) use( $template ) { if ( ! isset( $query['area'] ) || $query['area'] !== $template->get_area() ) { return $query_result; } $wp_block_template = $this->template_transformer->transform( $template ); $query_result[] = $wp_block_template; return $query_result; }, 10, 3 ); } } } BlockTemplateRegistry/BlockTemplateRegistry.php 0000644 00000003213 15073235522 0016016 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; /** * Block template registry. */ final class BlockTemplateRegistry { /** * Class instance. * * @var BlockTemplateRegistry|null */ private static $instance = null; /** * Templates. * * @var array */ protected $templates = array(); /** * Get the instance of the class. */ public static function get_instance(): BlockTemplateRegistry { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Register a single template. * * @param BlockTemplateInterface $template Template to register. * * @throws \ValueError If a template with the same ID already exists. */ public function register( BlockTemplateInterface $template ) { $id = $template->get_id(); if ( isset( $this->templates[ $id ] ) ) { throw new \ValueError( 'A template with the specified ID already exists in the registry.' ); } /** * Fires when a template is registered. * * @param BlockTemplateInterface $template Template that was registered. * * @since 8.2.0 */ do_action( 'woocommerce_block_template_register', $template ); $this->templates[ $id ] = $template; } /** * Get the registered templates. */ public function get_all_registered(): array { return $this->templates; } /** * Get a single registered template. * * @param string $id ID of the template. */ public function get_registered( $id ): BlockTemplateInterface { return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null; } } BlockTemplateRegistry/TemplateTransformer.php 0000644 00000002575 15073235522 0015547 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; /** * Template transformer. */ class TemplateTransformer { /** * Transform the WooCommerceBlockTemplate to a WP_Block_Template. * * @param object $block_template The product template. */ public function transform( BlockTemplateInterface $block_template ): \WP_Block_Template { $template = new \WP_Block_Template(); $template->id = $block_template->get_id(); $template->theme = 'woocommerce/woocommerce'; $template->content = $block_template->get_formatted_template(); $template->source = 'plugin'; $template->slug = $block_template->get_id(); $template->type = 'wp_template'; $template->title = $block_template->get_title(); $template->description = $block_template->get_description(); $template->status = 'publish'; $template->has_theme_file = true; $template->origin = 'plugin'; $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are. $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown. $template->area = $block_template->get_area(); return $template; } } BlockTemplates/BlockTemplate.php 0000644 00000001400 15073235522 0012713 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; /** * Block template class. */ class BlockTemplate extends AbstractBlockTemplate { /** * Get the template ID. */ public function get_id(): string { return 'woocommerce-block-template'; } /** * Add an inner block to this template. * * @param array $block_config The block data. */ public function add_block( array $block_config ): BlockInterface { $block = new Block( $block_config, $this->get_root_template(), $this ); return $this->add_inner_block( $block ); } } BlockTemplates/BlockFormattedTemplateTrait.php 0000644 00000003335 15073235522 0015576 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; /** * Trait for block formatted template. */ trait BlockFormattedTemplateTrait { /** * Get the block configuration as a formatted template. * * @return array The block configuration as a formatted template. */ public function get_formatted_template(): array { $arr = array( $this->get_name(), array_merge( $this->get_attributes(), array( '_templateBlockId' => $this->get_id(), '_templateBlockOrder' => $this->get_order(), ), ! empty( $this->get_hide_conditions() ) ? array( '_templateBlockHideConditions' => $this->get_formatted_hide_conditions(), ) : array(), ! empty( $this->get_disable_conditions() ) ? array( '_templateBlockDisableConditions' => $this->get_formatted_disable_conditions(), ) : array(), ), ); return $arr; } /** * Get the block hide conditions formatted for inclusion in a formatted template. */ private function get_formatted_hide_conditions(): array { return $this->format_conditions( $this->get_hide_conditions() ); } /** * Get the block disable conditions formatted for inclusion in a formatted template. */ private function get_formatted_disable_conditions(): array { return $this->format_conditions( $this->get_disable_conditions() ); } /** * Formats conditions in the expected format to include in the template. * * @param array $conditions The conditions to format. */ private function format_conditions( $conditions ): array { $formatted_expressions = array_map( function( $condition ) { return array( 'expression' => $condition['expression'], ); }, array_values( $conditions ) ); return $formatted_expressions; } } BlockTemplates/AbstractBlock.php 0000644 00000021012 15073235522 0012704 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface; /** * Block configuration used to specify blocks in BlockTemplate. */ class AbstractBlock implements BlockInterface { use BlockFormattedTemplateTrait; /** * The block name. * * @var string */ private $name; /** * The block ID. * * @var string */ private $id; /** * The block order. * * @var int */ private $order = 10000; /** * The block attributes. * * @var array */ private $attributes = array(); /** * The block hide conditions. * * @var array */ private $hide_conditions = array(); /** * The block hide conditions counter. * * @var int */ private $hide_conditions_counter = 0; /** * The block disable conditions. * * @var array */ private $disable_conditions = array(); /** * The block disable conditions counter. * * @var int */ private $disable_conditions_counter = 0; /** * The block template that this block belongs to. * * @var BlockTemplate */ private $root_template; /** * The parent container. * * @var ContainerInterface */ private $parent; /** * Block constructor. * * @param array $config The block configuration. * @param BlockTemplateInterface $root_template The block template that this block belongs to. * @param BlockContainerInterface|null $parent The parent block container. * * @throws \ValueError If the block configuration is invalid. * @throws \ValueError If the parent block container does not belong to the same template as the block. */ public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) { $this->validate( $config, $root_template, $parent ); $this->root_template = $root_template; $this->parent = is_null( $parent ) ? $root_template : $parent; $this->name = $config[ self::NAME_KEY ]; if ( ! isset( $config[ self::ID_KEY ] ) ) { $this->id = $this->root_template->generate_block_id( $this->get_name() ); } else { $this->id = $config[ self::ID_KEY ]; } if ( isset( $config[ self::ORDER_KEY ] ) ) { $this->order = $config[ self::ORDER_KEY ]; } if ( isset( $config[ self::ATTRIBUTES_KEY ] ) ) { $this->attributes = $config[ self::ATTRIBUTES_KEY ]; } if ( isset( $config[ self::HIDE_CONDITIONS_KEY ] ) ) { foreach ( $config[ self::HIDE_CONDITIONS_KEY ] as $hide_condition ) { $this->add_hide_condition( $hide_condition['expression'] ); } } if ( isset( $config[ self::DISABLE_CONDITIONS_KEY ] ) ) { foreach ( $config[ self::DISABLE_CONDITIONS_KEY ] as $disable_condition ) { $this->add_disable_condition( $disable_condition['expression'] ); } } } /** * Validate block configuration. * * @param array $config The block configuration. * @param BlockTemplateInterface $root_template The block template that this block belongs to. * @param ContainerInterface|null $parent The parent block container. * * @throws \ValueError If the block configuration is invalid. * @throws \ValueError If the parent block container does not belong to the same template as the block. */ protected function validate( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) { if ( isset( $parent ) && ( $parent->get_root_template() !== $root_template ) ) { throw new \ValueError( 'The parent block must belong to the same template as the block.' ); } if ( ! isset( $config[ self::NAME_KEY ] ) || ! is_string( $config[ self::NAME_KEY ] ) ) { throw new \ValueError( 'The block name must be specified.' ); } if ( isset( $config[ self::ORDER_KEY ] ) && ! is_int( $config[ self::ORDER_KEY ] ) ) { throw new \ValueError( 'The block order must be an integer.' ); } if ( isset( $config[ self::ATTRIBUTES_KEY ] ) && ! is_array( $config[ self::ATTRIBUTES_KEY ] ) ) { throw new \ValueError( 'The block attributes must be an array.' ); } } /** * Get the block name. */ public function get_name(): string { return $this->name; } /** * Get the block ID. */ public function get_id(): string { return $this->id; } /** * Get the block order. */ public function get_order(): int { return $this->order; } /** * Set the block order. * * @param int $order The block order. */ public function set_order( int $order ) { $this->order = $order; } /** * Get the block attributes. */ public function get_attributes(): array { return $this->attributes; } /** * Set the block attributes. * * @param array $attributes The block attributes. */ public function set_attributes( array $attributes ) { $this->attributes = $attributes; } /** * Get the template that this block belongs to. */ public function &get_root_template(): BlockTemplateInterface { return $this->root_template; } /** * Get the parent block container. */ public function &get_parent(): ContainerInterface { return $this->parent; } /** * Remove the block from its parent. */ public function remove() { $this->parent->remove_block( $this->id ); } /** * Check if the block is detached from its parent block container or the template it belongs to. * * @return bool True if the block is detached from its parent block container or the template it belongs to. */ public function is_detached(): bool { $is_in_parent = $this->parent->get_block( $this->id ) === $this; $is_in_root_template = $this->get_root_template()->get_block( $this->id ) === $this; return ! ( $is_in_parent && $is_in_root_template ); } /** * Add a hide condition to the block. * * The hide condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden. * See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details. * * @param string $expression An expression, which if true, will hide the block. */ public function add_hide_condition( string $expression ): string { $key = 'k' . $this->hide_conditions_counter; $this->hide_conditions_counter++; // Storing the expression in an array to allow for future expansion // (such as adding the plugin that added the condition). $this->hide_conditions[ $key ] = array( 'expression' => $expression, ); /** * Action called after a hide condition is added to a block. * * @param BlockInterface $block The block. * * @since 8.4.0 */ do_action( 'woocommerce_block_template_after_add_hide_condition', $this ); return $key; } /** * Remove a hide condition from the block. * * @param string $key The key of the hide condition to remove. */ public function remove_hide_condition( string $key ) { unset( $this->hide_conditions[ $key ] ); /** * Action called after a hide condition is removed from a block. * * @param BlockInterface $block The block. * * @since 8.4.0 */ do_action( 'woocommerce_block_template_after_remove_hide_condition', $this ); } /** * Get the hide conditions of the block. */ public function get_hide_conditions(): array { return $this->hide_conditions; } /** * Add a disable condition to the block. * * The disable condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden. * See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details. * * @param string $expression An expression, which if true, will disable the block. */ public function add_disable_condition( string $expression ): string { $key = 'k' . $this->disable_conditions_counter; $this->disable_conditions_counter++; // Storing the expression in an array to allow for future expansion // (such as adding the plugin that added the condition). $this->disable_conditions[ $key ] = array( 'expression' => $expression, ); return $key; } /** * Remove a disable condition from the block. * * @param string $key The key of the disable condition to remove. */ public function remove_disable_condition( string $key ) { unset( $this->disable_conditions[ $key ] ); } /** * Get the disable conditions of the block. */ public function get_disable_conditions(): array { return $this->disable_conditions; } } BlockTemplates/BlockContainerTrait.php 0000644 00000024122 15073235522 0014074 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface; /** * Trait for block containers. */ trait BlockContainerTrait { use BlockFormattedTemplateTrait { get_formatted_template as get_block_formatted_template; } /** * The inner blocks. * * @var BlockInterface[] */ private $inner_blocks = array(); // phpcs doesn't take into account exceptions thrown by called methods. // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Add a block to the block container. * * @param BlockInterface $block The block. * * @throws \ValueError If the block configuration is invalid. * @throws \ValueError If a block with the specified ID already exists in the template. * @throws \UnexpectedValueException If the block container is not the parent of the block. * @throws \UnexpectedValueException If the block container's root template is not the same as the block's root template. */ protected function &add_inner_block( BlockInterface $block ): BlockInterface { if ( $block->get_parent() !== $this ) { throw new \UnexpectedValueException( 'The block container is not the parent of the block.' ); } if ( $block->get_root_template() !== $this->get_root_template() ) { throw new \UnexpectedValueException( 'The block container\'s root template is not the same as the block\'s root template.' ); } $is_detached = method_exists( $this, 'is_detached' ) && $this->is_detached(); if ( ! $is_detached ) { $this->get_root_template()->cache_block( $block ); } $this->inner_blocks[] = &$block; $this->do_after_add_block_action( $block ); $this->do_after_add_specific_block_action( $block ); return $block; } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Checks if a block is a descendant of the block container. * * @param BlockInterface $block The block. */ private function is_block_descendant( BlockInterface $block ): bool { $parent = $block->get_parent(); if ( $parent === $this ) { return true; } if ( ! $parent instanceof BlockInterface ) { return false; } return $this->is_block_descendant( $parent ); } /** * Get a block by ID. * * @param string $block_id The block ID. */ public function get_block( string $block_id ): ?BlockInterface { foreach ( $this->inner_blocks as $block ) { if ( $block->get_id() === $block_id ) { return $block; } } foreach ( $this->inner_blocks as $block ) { if ( $block instanceof ContainerInterface ) { $block = $block->get_block( $block_id ); if ( $block ) { return $block; } } } return null; } /** * Remove a block from the block container. * * @param string $block_id The block ID. * * @throws \UnexpectedValueException If the block container is not an ancestor of the block. */ public function remove_block( string $block_id ) { $root_template = $this->get_root_template(); $block = $root_template->get_block( $block_id ); if ( ! $block ) { return; } if ( ! $this->is_block_descendant( $block ) ) { throw new \UnexpectedValueException( 'The block container is not an ancestor of the block.' ); } // If the block is a container, remove all of its blocks. if ( $block instanceof ContainerInterface ) { $block->remove_blocks(); } $parent = $block->get_parent(); $parent->remove_inner_block( $block ); } /** * Remove all blocks from the block container. */ public function remove_blocks() { array_map( function ( BlockInterface $block ) { $this->remove_block( $block->get_id() ); }, $this->inner_blocks ); } /** * Remove a block from the block container's inner blocks. This is an internal method and should not be called directly * except for from the BlockContainerTrait's remove_block() method. * * @param BlockInterface $block The block. */ public function remove_inner_block( BlockInterface $block ) { // Remove block from root template's cache. $root_template = $this->get_root_template(); $root_template->uncache_block( $block->get_id() ); $this->inner_blocks = array_filter( $this->inner_blocks, function ( BlockInterface $inner_block ) use ( $block ) { return $inner_block !== $block; } ); $this->do_after_remove_block_action( $block ); $this->do_after_remove_specific_block_action( $block ); } /** * Get the inner blocks sorted by order. */ private function get_inner_blocks_sorted_by_order(): array { $sorted_inner_blocks = $this->inner_blocks; usort( $sorted_inner_blocks, function( BlockInterface $a, BlockInterface $b ) { return $a->get_order() <=> $b->get_order(); } ); return $sorted_inner_blocks; } /** * Get the inner blocks as a formatted template. */ public function get_formatted_template(): array { $arr = $this->get_block_formatted_template(); $inner_blocks = $this->get_inner_blocks_sorted_by_order(); if ( ! empty( $inner_blocks ) ) { $arr[] = array_map( function( BlockInterface $block ) { return $block->get_formatted_template(); }, $inner_blocks ); } return $arr; } /** * Do the `woocommerce_block_template_after_add_block` action. * Handle exceptions thrown by the action. * * @param BlockInterface $block The block. */ private function do_after_add_block_action( BlockInterface $block ) { try { /** * Action called after a block is added to a block container. * * This action can be used to perform actions after a block is added to the block container, * such as adding a dependent block. * * @param BlockInterface $block The block. * * @since 8.2.0 */ do_action( 'woocommerce_block_template_after_add_block', $block ); } catch ( \Exception $e ) { $this->do_after_add_block_error_action( $block, 'woocommerce_block_template_after_add_block', $e ); } } /** * Do the `woocommerce_block_template_area_{template_area}_after_add_block_{block_id}` action. * Handle exceptions thrown by the action. * * @param BlockInterface $block The block. */ private function do_after_add_specific_block_action( BlockInterface $block ) { try { /** * Action called after a specific block is added to a template with a specific area. * * This action can be used to perform actions after a specific block is added to a template with a specific area, * such as adding a dependent block. * * @param BlockInterface $block The block. * * @since 8.2.0 */ do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}", $block ); } catch ( \Exception $e ) { $this->do_after_add_block_error_action( $block, "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}", $e ); } } /** * Do the `woocommerce_block_after_add_block_error` action. * * @param BlockInterface $block The block. * @param string $action The action that threw the exception. * @param \Exception $e The exception. */ private function do_after_add_block_error_action( BlockInterface $block, string $action, \Exception $e ) { /** * Action called after an exception is thrown by a `woocommerce_block_template_after_add_block` action hook. * * @param BlockInterface $block The block. * @param string $action The action that threw the exception. * @param \Exception $exception The exception. * * @since 8.4.0 */ do_action( 'woocommerce_block_template_after_add_block_error', $block, $action, $e, ); } /** * Do the `woocommerce_block_template_after_remove_block` action. * Handle exceptions thrown by the action. * * @param BlockInterface $block The block. */ private function do_after_remove_block_action( BlockInterface $block ) { try { /** * Action called after a block is removed from a block container. * * This action can be used to perform actions after a block is removed from the block container, * such as removing a dependent block. * * @param BlockInterface $block The block. * * @since 8.2.0 */ do_action( 'woocommerce_block_template_after_remove_block', $block ); } catch ( \Exception $e ) { $this->do_after_remove_block_error_action( $block, 'woocommerce_block_template_after_remove_block', $e ); } } /** * Do the `woocommerce_block_template_area_{template_area}_after_remove_block_{block_id}` action. * Handle exceptions thrown by the action. * * @param BlockInterface $block The block. */ private function do_after_remove_specific_block_action( BlockInterface $block ) { try { /** * Action called after a specific block is removed from a template with a specific area. * * This action can be used to perform actions after a specific block is removed from a template with a specific area, * such as removing a dependent block. * * @param BlockInterface $block The block. * * @since 8.2.0 */ do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}", $block ); } catch ( \Exception $e ) { $this->do_after_remove_block_error_action( $block, "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}", $e ); } } /** * Do the `woocommerce_block_after_remove_block_error` action. * * @param BlockInterface $block The block. * @param string $action The action that threw the exception. * @param \Exception $e The exception. */ private function do_after_remove_block_error_action( BlockInterface $block, string $action, \Exception $e ) { /** * Action called after an exception is thrown by a `woocommerce_block_template_after_remove_block` action hook. * * @param BlockInterface $block The block. * @param string $action The action that threw the exception. * @param \Exception $exception The exception. * * @since 8.4.0 */ do_action( 'woocommerce_block_template_after_remove_block_error', $block, $action, $e, ); } } BlockTemplates/AbstractBlockTemplate.php 0000644 00000006152 15073235522 0014410 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; /** * Block template class. */ abstract class AbstractBlockTemplate implements BlockTemplateInterface { use BlockContainerTrait; /** * Get the template ID. */ abstract public function get_id(): string; /** * Get the template title. */ public function get_title(): string { return ''; } /** * Get the template description. */ public function get_description(): string { return ''; } /** * Get the template area. */ public function get_area(): string { return 'uncategorized'; } /** * The block cache. * * @var BlockInterface[] */ private $block_cache = []; /** * Get a block by ID. * * @param string $block_id The block ID. */ public function get_block( string $block_id ): ?BlockInterface { return $this->block_cache[ $block_id ] ?? null; } /** * Caches a block in the template. This is an internal method and should not be called directly * except for from the BlockContainerTrait's add_inner_block() method. * * @param BlockInterface $block The block to cache. * * @throws \ValueError If a block with the specified ID already exists in the template. * @throws \ValueError If the block template that the block belongs to is not this template. * * @ignore */ public function cache_block( BlockInterface &$block ) { $id = $block->get_id(); if ( isset( $this->block_cache[ $id ] ) ) { throw new \ValueError( 'A block with the specified ID already exists in the template.' ); } if ( $block->get_root_template() !== $this ) { throw new \ValueError( 'The block template that the block belongs to must be the same as this template.' ); } $this->block_cache[ $id ] = $block; } /** * Uncaches a block in the template. This is an internal method and should not be called directly * except for from the BlockContainerTrait's remove_block() method. * * @param string $block_id The block ID. * * @ignore */ public function uncache_block( string $block_id ) { if ( isset( $this->block_cache[ $block_id ] ) ) { unset( $this->block_cache[ $block_id ] ); } } /** * Generate a block ID based on a base. * * @param string $id_base The base to use when generating an ID. * @return string */ public function generate_block_id( string $id_base ): string { $instance_count = 0; do { $instance_count++; $block_id = $id_base . '-' . $instance_count; } while ( isset( $this->block_cache[ $block_id ] ) ); return $block_id; } /** * Get the root template. */ public function &get_root_template(): BlockTemplateInterface { return $this; } /** * Get the inner blocks as a formatted template. */ public function get_formatted_template(): array { $inner_blocks = $this->get_inner_blocks_sorted_by_order(); $inner_blocks_formatted_template = array_map( function( BlockInterface $block ) { return $block->get_formatted_template(); }, $inner_blocks ); return $inner_blocks_formatted_template; } } BlockTemplates/BlockTemplateLogger.php 0000644 00000034070 15073235522 0014064 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface; /** * Logger for block template modifications. */ class BlockTemplateLogger { const BLOCK_ADDED = 'block_added'; const BLOCK_REMOVED = 'block_removed'; const BLOCK_MODIFIED = 'block_modified'; const BLOCK_ADDED_TO_DETACHED_CONTAINER = 'block_added_to_detached_container'; const HIDE_CONDITION_ADDED = 'hide_condition_added'; const HIDE_CONDITION_REMOVED = 'hide_condition_removed'; const HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK = 'hide_condition_added_to_detached_block'; const ERROR_AFTER_BLOCK_ADDED = 'error_after_block_added'; const ERROR_AFTER_BLOCK_REMOVED = 'error_after_block_removed'; const LOG_HASH_TRANSIENT_BASE_NAME = 'wc_block_template_events_log_hash_'; /** * Event types. * * @var array */ public static $event_types = array( self::BLOCK_ADDED => array( 'level' => \WC_Log_Levels::DEBUG, 'message' => 'Block added to template.', ), self::BLOCK_REMOVED => array( 'level' => \WC_Log_Levels::NOTICE, 'message' => 'Block removed from template.', ), self::BLOCK_MODIFIED => array( 'level' => \WC_Log_Levels::NOTICE, 'message' => 'Block modified in template.', ), self::BLOCK_ADDED_TO_DETACHED_CONTAINER => array( 'level' => \WC_Log_Levels::WARNING, 'message' => 'Block added to detached container. Block will not be included in the template, since the container will not be included in the template.', ), self::HIDE_CONDITION_ADDED => array( 'level' => \WC_Log_Levels::NOTICE, 'message' => 'Hide condition added to block.', ), self::HIDE_CONDITION_REMOVED => array( 'level' => \WC_Log_Levels::NOTICE, 'message' => 'Hide condition removed from block.', ), self::HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK => array( 'level' => \WC_Log_Levels::WARNING, 'message' => 'Hide condition added to detached block. Block will not be included in the template, so the hide condition is not needed.', ), self::ERROR_AFTER_BLOCK_ADDED => array( 'level' => \WC_Log_Levels::WARNING, 'message' => 'Error after block added to template.', ), self::ERROR_AFTER_BLOCK_REMOVED => array( 'level' => \WC_Log_Levels::WARNING, 'message' => 'Error after block removed from template.', ), ); /** * Singleton instance. * * @var BlockTemplateLogger */ protected static $instance = null; /** * Logger instance. * * @var \WC_Logger */ protected $logger = null; /** * All template events. * * @var array */ private $all_template_events = array(); /** * Templates. * * @var array */ private $templates = array(); /** * Threshold severity. * * @var int */ private $threshold_severity = null; /** * Get the singleton instance. */ public static function get_instance(): BlockTemplateLogger { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. */ protected function __construct() { $this->logger = wc_get_logger(); $threshold = get_option( 'woocommerce_block_template_logging_threshold', \WC_Log_Levels::WARNING ); if ( ! \WC_Log_Levels::is_valid_level( $threshold ) ) { $threshold = \WC_Log_Levels::INFO; } $this->threshold_severity = \WC_Log_Levels::get_level_severity( $threshold ); add_action( 'woocommerce_block_template_after_add_block', function ( BlockInterface $block ) { $is_detached = method_exists( $block->get_parent(), 'is_detached' ) && $block->get_parent()->is_detached(); $this->log( $is_detached ? $this::BLOCK_ADDED_TO_DETACHED_CONTAINER : $this::BLOCK_ADDED, $block, ); }, 0, ); add_action( 'woocommerce_block_template_after_remove_block', function ( BlockInterface $block ) { $this->log( $this::BLOCK_REMOVED, $block, ); }, 0, ); add_action( 'woocommerce_block_template_after_add_hide_condition', function ( BlockInterface $block ) { $this->log( $block->is_detached() ? $this::HIDE_CONDITION_ADDED_TO_DETACHED_BLOCK : $this::HIDE_CONDITION_ADDED, $block, ); }, 0 ); add_action( 'woocommerce_block_template_after_remove_hide_condition', function ( BlockInterface $block ) { $this->log( $this::HIDE_CONDITION_REMOVED, $block, ); }, 0 ); add_action( 'woocommerce_block_template_after_add_block_error', function ( BlockInterface $block, string $action, \Exception $exception ) { $this->log( $this::ERROR_AFTER_BLOCK_ADDED, $block, array( 'action' => $action, 'exception' => $exception, ), ); }, 0, 3 ); add_action( 'woocommerce_block_template_after_remove_block_error', function ( BlockInterface $block, string $action, \Exception $exception ) { $this->log( $this::ERROR_AFTER_BLOCK_REMOVED, $block, array( 'action' => $action, 'exception' => $exception, ), ); }, 0, 3 ); } /** * Get all template events for a given template. * * @param string $template_id Template ID. */ public function get_formatted_template_events( string $template_id ): array { if ( ! isset( $this->all_template_events[ $template_id ] ) ) { return array(); } $template_events = $this->all_template_events[ $template_id ]; $template = $this->templates[ $template_id ]; $formatted_template_events = array(); foreach ( $template_events as $template_event ) { $container = $template_event['container']; $block = $template_event['block']; $formatted_template_events[] = array( 'level' => $template_event['level'], 'event_type' => $template_event['event_type'], 'message' => $template_event['message'], 'container' => $container instanceof BlockInterface ? array( 'id' => $container->get_id(), 'name' => $container->get_name(), ) : null, 'block' => array( 'id' => $block->get_id(), 'name' => $block->get_name(), ), 'additional_info' => $this->format_info( $template_event['additional_info'] ), ); } return $formatted_template_events; } /** * Log all template events for a given template to the log file. * * @param string $template_id Template ID. */ public function log_template_events_to_file( string $template_id ) { if ( ! isset( $this->all_template_events[ $template_id ] ) ) { return; } $template_events = $this->all_template_events[ $template_id ]; $hash = $this->generate_template_events_hash( $template_events ); if ( ! $this->has_template_events_changed( $template_id, $hash ) ) { // Nothing has changed since the last time this was logged, // so don't log it again. return; } $this->set_template_events_log_hash( $template_id, $hash ); $template = $this->templates[ $template_id ]; foreach ( $template_events as $template_event ) { $info = array_merge( array( 'template' => $template, 'container' => $template_event['container'], 'block' => $template_event['block'], ), $template_event['additional_info'] ); $message = $this->format_message( $template_event['message'], $info ); $this->logger->log( $template_event['level'], $message, array( 'source' => 'block_template' ) ); } } /** * Has the template events changed since the last time they were logged? * * @param string $template_id Template ID. * @param string $events_hash Events hash. */ private function has_template_events_changed( string $template_id, string $events_hash ) { $previous_hash = get_transient( self::LOG_HASH_TRANSIENT_BASE_NAME . $template_id ); return $previous_hash !== $events_hash; } /** * Generate a hash for a given set of template events. * * @param array $template_events Template events. */ private function generate_template_events_hash( array $template_events ): string { return md5( wp_json_encode( $template_events ) ); } /** * Set the template events hash for a given template. * * @param string $template_id Template ID. * @param string $hash Hash of template events. */ private function set_template_events_log_hash( string $template_id, string $hash ) { set_transient( self::LOG_HASH_TRANSIENT_BASE_NAME . $template_id, $hash ); } /** * Log an event. * * @param string $event_type Event type. * @param BlockInterface $block Block. * @param array $additional_info Additional info. */ private function log( string $event_type, BlockInterface $block, $additional_info = array() ) { if ( ! isset( self::$event_types[ $event_type ] ) ) { /* translators: 1: WC_Logger::log 2: level */ wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid event type "%2$s".', 'woocommerce' ), '<code>BlockTemplateLogger::log</code>', $event_type ), '8.4' ); } $event_type_info = isset( self::$event_types[ $event_type ] ) ? array_merge( self::$event_types[ $event_type ], array( 'event_type' => $event_type, ) ) : array( 'level' => \WC_Log_Levels::ERROR, 'event_type' => $event_type, 'message' => 'Unknown error.', ); if ( ! $this->should_handle( $event_type_info['level'] ) ) { return; } $template = $block->get_root_template(); $container = $block->get_parent(); $this->add_template_event( $event_type_info, $template, $container, $block, $additional_info ); } /** * Should the logger handle a given level? * * @param int $level Level to check. */ private function should_handle( $level ) { return $this->threshold_severity <= \WC_Log_Levels::get_level_severity( $level ); } /** * Add a template event. * * @param array $event_type_info Event type info. * @param BlockTemplateInterface $template Template. * @param ContainerInterface $container Container. * @param BlockInterface $block Block. * @param array $additional_info Additional info. */ private function add_template_event( array $event_type_info, BlockTemplateInterface $template, ContainerInterface $container, BlockInterface $block, array $additional_info = array() ) { $template_id = $template->get_id(); if ( ! isset( $this->all_template_events[ $template_id ] ) ) { $this->all_template_events[ $template_id ] = array(); $this->templates[ $template_id ] = $template; } $template_events = &$this->all_template_events[ $template_id ]; $template_events[] = array( 'level' => $event_type_info['level'], 'event_type' => $event_type_info['event_type'], 'message' => $event_type_info['message'], 'container' => $container, 'block' => $block, 'additional_info' => $additional_info, ); } /** * Format a message for logging. * * @param string $message Message to log. * @param array $info Additional info to log. */ private function format_message( string $message, array $info = array() ): string { $formatted_message = sprintf( "%s\n%s", $message, // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r print_r( $this->format_info( $info ), true ), ); return $formatted_message; } /** * Format info for logging. * * @param array $info Info to log. */ private function format_info( array $info ): array { $formatted_info = $info; if ( isset( $info['exception'] ) && $info['exception'] instanceof \Exception ) { $formatted_info['exception'] = $this->format_exception( $info['exception'] ); } if ( isset( $info['container'] ) ) { if ( $info['container'] instanceof BlockContainerInterface ) { $formatted_info['container'] = $this->format_block( $info['container'] ); } elseif ( $info['container'] instanceof BlockTemplateInterface ) { $formatted_info['container'] = $this->format_template( $info['container'] ); } elseif ( $info['container'] instanceof BlockInterface ) { $formatted_info['container'] = $this->format_block( $info['container'] ); } } if ( isset( $info['block'] ) && $info['block'] instanceof BlockInterface ) { $formatted_info['block'] = $this->format_block( $info['block'] ); } if ( isset( $info['template'] ) && $info['template'] instanceof BlockTemplateInterface ) { $formatted_info['template'] = $this->format_template( $info['template'] ); } return $formatted_info; } /** * Format an exception for logging. * * @param \Exception $exception Exception to format. */ private function format_exception( \Exception $exception ): array { return array( 'message' => $exception->getMessage(), 'source' => "{$exception->getFile()}: {$exception->getLine()}", // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 'trace' => print_r( $this->format_exception_trace( $exception->getTrace() ), true ), ); } /** * Format an exception trace for logging. * * @param array $trace Exception trace to format. */ private function format_exception_trace( array $trace ): array { $formatted_trace = array(); foreach ( $trace as $source ) { $formatted_trace[] = "{$source['file']}: {$source['line']}"; } return $formatted_trace; } /** * Format a block template for logging. * * @param BlockTemplateInterface $template Template to format. */ private function format_template( BlockTemplateInterface $template ): string { return "{$template->get_id()} (area: {$template->get_area()})"; } /** * Format a block for logging. * * @param BlockInterface $block Block to format. */ private function format_block( BlockInterface $block ): string { return "{$block->get_id()} (name: {$block->get_name()})"; } } BlockTemplates/Block.php 0000644 00000001356 15073235522 0011231 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates; use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface; use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface; /** * Generic block with container properties to be used in BlockTemplate. */ class Block extends AbstractBlock implements BlockContainerInterface { use BlockContainerTrait; /** * Add an inner block to this block. * * @param array $block_config The block data. */ public function &add_block( array $block_config ): BlockInterface { $block = new Block( $block_config, $this->get_root_template(), $this ); return $this->add_inner_block( $block ); } } Settings.php 0000644 00000031370 15073235522 0007065 0 ustar 00 <?php /** * WooCommerce Settings. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\API\Plugins; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore; use Automattic\WooCommerce\Admin\PluginsHelper; use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit; use Automattic\WooCommerce\Utilities\FeaturesUtil; use WC_Marketplace_Suggestions; /** * Contains logic in regards to WooCommerce Admin Settings. */ class Settings { /** * Class instance. * * @var Settings 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() { // Old settings injection. add_filter( 'woocommerce_components_settings', array( $this, 'add_component_settings' ) ); // New settings injection. add_filter( 'woocommerce_admin_shared_settings', array( $this, 'add_component_settings' ) ); add_filter( 'woocommerce_settings_groups', array( $this, 'add_settings_group' ) ); add_filter( 'woocommerce_settings-wc_admin', array( $this, 'add_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. */ private 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 ); } /** * 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(); //phpcs:ignore 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() ), ) ); } /** * Hooks extra necessary data into the component settings array already set in WooCommerce core. * * @param array $settings Array of component settings. * @return array Array of component settings. */ public function add_component_settings( $settings ) { if ( ! is_admin() ) { return $settings; } if ( ! function_exists( 'wc_blocks_container' ) ) { global $wp_locale; // inject data not available via older versions of wc_blocks/woo. $settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() ); $settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() ); $settings['currency'] = self::get_currency_settings(); $settings['locale'] = array( 'siteLocale' => isset( $settings['siteLocale'] ) ? $settings['siteLocale'] : get_locale(), 'userLocale' => isset( $settings['l10n']['userLocale'] ) ? $settings['l10n']['userLocale'] : get_user_locale(), 'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] ) ? $settings['l10n']['weekdaysShort'] : array_values( $wp_locale->weekday_abbrev ), ); } //phpcs:ignore $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' ); } //phpcs:ignore $preload_options = apply_filters( 'woocommerce_admin_preload_options', array() ); if ( ! empty( $preload_options ) ) { foreach ( $preload_options as $option ) { $settings['preloadOptions'][ $option ] = get_option( $option ); } } //phpcs:ignore $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 = array(); 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' ); /** * Deprecate wcAdminAssetUrl as we no longer need it after The Merge. * Use wcAssetUrl instead. * * @deprecated 6.7.0 * @var string */ $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'] = $this->get_unregistered_order_statuses(); // The separator used for attributes found in Variation titles. //phpcs:ignore $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'] : array(); 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 = $this->get_custom_settings( $settings ); if ( PageController::is_embed_page() ) { $settings['embedBreadcrumbs'] = wc_admin_get_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' ); $settings['wc_helper_nonces'] = array( 'refresh' => wp_create_nonce( 'refresh' ), ); $settings['features'] = $this->get_features(); $settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible(); $settings['gutenberg_version'] = defined( 'GUTENBERG_VERSION' ) ? constant( 'GUTENBERG_VERSION' ) : 0; return $settings; } /** * Removes non necesary feature properties for the client side. * * @return array */ public function get_features() { $features = FeaturesUtil::get_features( true, true ); $new_features = array(); foreach ( array_keys( $features ) as $feature_id ) { $new_features[ $feature_id ] = array( 'is_enabled' => $features[ $feature_id ]['is_enabled'], 'is_experimental' => $features[ $feature_id ]['is_experimental'] ?? false, ); } return $new_features; } /** * Register the admin settings for use in the WC REST API * * @param array $groups Array of setting groups. * @return array */ public 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 function add_settings( $settings ) { $unregistered_statuses = $this->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', ); $settings[] = array( 'id' => 'woocommerce_date_type', 'option_key' => 'woocommerce_date_type', 'label' => __( 'Date Type', 'woocommerce' ), 'description' => __( 'Database date field considered for Revenue and Orders reports', 'woocommerce' ), 'type' => 'select', 'options' => array( 'date_created' => 'date_created', 'date_paid' => 'date_paid', 'date_completed' => 'date_completed', ), ); return $settings; } /** * Gets custom settings used for WC Admin. * * @param array $settings Array of settings to merge into. * @return array */ private 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; } } MobileAppBanner.php 0000644 00000001674 15073235522 0010267 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * Determine if the mobile app banner shows on Android devices */ class MobileAppBanner { /** * Class instance. * * @var Analytics instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) ); } /** * Adds fields so that we can store user preferences for the mobile app banner * * @param array $user_data_fields User data fields. * @return array */ public function add_user_data_fields( $user_data_fields ) { return array_merge( $user_data_fields, array( 'android_app_banner_dismissed', ) ); } } ShippingLabelBannerDisplayRules.php 0000644 00000012266 15073235522 0013500 0 ustar 00 <?php /** * WooCommerce Shipping Label Banner Display Rules. */ namespace Automattic\WooCommerce\Internal\Admin; /** * Determines whether or not the Shipping Label Banner should be displayed */ class ShippingLabelBannerDisplayRules { /** * Holds the installed Jetpack version. * * @var string */ private $jetpack_version; /** * Whether or not the installed Jetpack is connected. * * @var bool */ private $jetpack_connected; /** * Holds the installed WooCommerce Shipping & Tax version. * * @var string */ private $wcs_version; /** * Whether or not there're plugins installed incompatible with the banner. * * @var bool */ private $no_incompatible_plugins_installed; /** * Whether or not the WooCommerce Shipping & Tax ToS has been accepted. * * @var bool */ private $wcs_tos_accepted; /** * Minimum supported Jetpack version. * * @var string */ private $min_jetpack_version = '4.4'; /** * Minimum supported WooCommerce Shipping & Tax version. * * @var string */ private $min_wcs_version = '1.22.5'; /** * Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm * * @var array */ private $supported_countries = array( 'US', 'AS', 'PR', 'VI', 'GU', 'MP', 'UM', 'FM', 'MH' ); /** * Array of supported currency codes. * * @var array */ private $supported_currencies = array( 'USD' ); /** * Constructor. * * @param string $jetpack_version Installed Jetpack version to check. * @param bool $jetpack_connected Is Jetpack connected?. * @param string $wcs_version Installed WooCommerce Shipping & Tax version to check. * @param bool $wcs_tos_accepted WooCommerce Shipping & Tax Terms of Service accepted?. * @param bool $incompatible_plugins_installed Are there any incompatible plugins installed?. */ public function __construct( $jetpack_version, $jetpack_connected, $wcs_version, $wcs_tos_accepted, $incompatible_plugins_installed ) { $this->jetpack_version = $jetpack_version; $this->jetpack_connected = $jetpack_connected; $this->wcs_version = $wcs_version; $this->wcs_tos_accepted = $wcs_tos_accepted; $this->no_incompatible_plugins_installed = ! $incompatible_plugins_installed; } /** * Determines whether banner is eligible for display (does not include a/b logic). */ public function should_display_banner() { return $this->banner_not_dismissed() && $this->jetpack_installed_and_active() && $this->jetpack_up_to_date() && $this->jetpack_connected && $this->no_incompatible_plugins_installed && $this->order_has_shippable_products() && $this->store_in_us_and_usd() && ( $this->wcs_not_installed() || ( $this->wcs_up_to_date() && ! $this->wcs_tos_accepted ) ); } /** * Checks if the banner was not dismissed by the user. * * @return bool */ private function banner_not_dismissed() { $dismissed_timestamp_ms = get_option( 'woocommerce_shipping_dismissed_timestamp' ); if ( ! is_numeric( $dismissed_timestamp_ms ) ) { return true; } $dismissed_timestamp_ms = intval( $dismissed_timestamp_ms ); $dismissed_timestamp = intval( round( $dismissed_timestamp_ms / 1000 ) ); $expired_timestamp = $dismissed_timestamp + 24 * 60 * 60; // 24 hours from click time $dismissed_for_good = -1 === $dismissed_timestamp_ms; $dismissed_24h = time() < $expired_timestamp; return ! $dismissed_for_good && ! $dismissed_24h; } /** * Checks if jetpack is installed and active. * * @return bool */ private function jetpack_installed_and_active() { return ! ! $this->jetpack_version; } /** * Checks if Jetpack version is supported. * * @return bool */ private function jetpack_up_to_date() { return version_compare( $this->jetpack_version, $this->min_jetpack_version, '>=' ); } /** * Checks if there's a shippable product in the current order. * * @return bool */ private function order_has_shippable_products() { $post = get_post(); if ( ! $post ) { return false; } $order = wc_get_order( get_post()->ID ); if ( ! $order ) { return false; } // At this point (no packaging data), only show if there's at least one existing and shippable product. foreach ( $order->get_items() as $item ) { if ( $item instanceof \WC_Order_Item_Product ) { $product = $item->get_product(); if ( $product && $product->needs_shipping() ) { return true; } } } return false; } /** * Checks if the store is in the US and has its default currency set to USD. * * @return bool */ private function store_in_us_and_usd() { $base_currency = get_woocommerce_currency(); $base_location = wc_get_base_location(); return in_array( $base_currency, $this->supported_currencies, true ) && in_array( $base_location['country'], $this->supported_countries, true ); } /** * Checks if WooCommerce Shipping & Tax is not installed. * * @return bool */ private function wcs_not_installed() { return ! $this->wcs_version; } /** * Checks if WooCommerce Shipping & Tax is up to date. */ private function wcs_up_to_date() { return $this->wcs_version && version_compare( $this->wcs_version, $this->min_wcs_version, '>=' ); } } WcPayWelcomePage.php 0000644 00000037067 15073235522 0010432 0 ustar 00 <?php namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\WCAdminHelper; use Automattic\WooCommerce\Admin\PageController; use WC_Abstract_Order; /** * Class WCPayWelcomePage * * @package Automattic\WooCommerce\Admin\Features */ class WcPayWelcomePage { const CACHE_TRANSIENT_NAME = 'wcpay_welcome_page_incentive'; const HAS_ORDERS_TRANSIENT_NAME = 'wcpay_incentive_store_has_orders'; const HAD_WCPAY_OPTION_NAME = 'wcpay_was_in_use'; /** * Plugin instance. * * @var WcPayWelcomePage */ protected static $instance = null; /** * Main Instance. */ public static function instance() { self::$instance = is_null( self::$instance ) ? new self() : self::$instance; return self::$instance; } /** * Eligible incentive for the store. * * @var array|null */ private $incentive = null; /** * WCPayWelcomePage constructor. */ public function __construct() { add_action( 'admin_menu', [ $this, 'register_payments_welcome_page' ] ); add_filter( 'woocommerce_admin_shared_settings', [ $this, 'shared_settings' ] ); add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] ); add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] ); } /** * Whether the WooPayments welcome page should be visible. * * @return boolean */ public function must_be_visible(): bool { // The WooPayments plugin must not be active. if ( $this->is_wcpay_active() ) { return false; } // Suggestions not disabled via a setting. if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) { return false; } /** * Filter allow marketplace suggestions. * * User can disable all suggestions via filter. * * @since 3.6.0 */ if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) { return false; } // An incentive must be available. if ( empty( $this->get_incentive() ) ) { return false; } // Incentive not manually dismissed. if ( $this->is_incentive_dismissed() ) { return false; } return true; } /** * Registers the WooPayments welcome page. */ public function register_payments_welcome_page() { global $menu; if ( ! $this->must_be_visible() ) { return; } $menu_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NTIiIGhlaWdodD0iNjg0Ij48cGF0aCBmaWxsPSIjYTJhYWIyIiBkPSJNODIgODZ2NTEyaDY4NFY4NlptMCA1OThjLTQ4IDAtODQtMzgtODQtODZWODZDLTIgMzggMzQgMCA4MiAwaDY4NGM0OCAwIDg0IDM4IDg0IDg2djUxMmMwIDQ4LTM2IDg2LTg0IDg2em0zODQtNTU2djQ0aDg2djg0SDM4MnY0NGgxMjhjMjQgMCA0MiAxOCA0MiA0MnYxMjhjMCAyNC0xOCA0Mi00MiA0MmgtNDR2NDRoLTg0di00NGgtODZ2LTg0aDE3MHYtNDRIMzM4Yy0yNCAwLTQyLTE4LTQyLTQyVjIxNGMwLTI0IDE4LTQyIDQyLTQyaDQ0di00NHoiLz48L3N2Zz4='; $menu_data = [ 'id' => 'wc-calypso-bridge-payments-welcome-page', 'title' => esc_html__( 'Payments', 'woocommerce' ), 'path' => '/wc-pay-welcome-page', 'position' => '56', 'nav_args' => [ 'title' => esc_html__( 'WooPayments', 'woocommerce' ), 'is_category' => false, 'menuId' => 'plugins', 'is_top_level' => true, ], 'icon' => $menu_icon, ]; wc_admin_register_page( $menu_data ); // Registering a top level menu via wc_admin_register_page doesn't work when the new // nav is enabled. The new nav disabled everything, except the 'WooCommerce' menu. // We need to register this menu via add_menu_page so that it doesn't become a child of // WooCommerce menu. if ( get_option( 'woocommerce_navigation_enabled', 'no' ) === 'yes' ) { $menu_with_nav_data = [ esc_html__( 'Payments', 'woocommerce' ), esc_html__( 'Payments', 'woocommerce' ), 'view_woocommerce_reports', 'admin.php?page=wc-admin&path=/wc-pay-welcome-page', null, $menu_icon, 56, ]; call_user_func_array( 'add_menu_page', $menu_with_nav_data ); } // Add badge. $badge = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>'; foreach ( $menu as $index => $menu_item ) { // Only add the badge markup if not already present and the menu item is the WooPayments menu item. if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] || 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] ) ) { $menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // One menu item with a badge is more than enough. break; } } } /** * Adds shared settings for the WooPayments incentive. * * @param array $settings Shared settings. * @return array */ public function shared_settings( $settings ): array { // Return early if not on a wc-admin powered page. if ( ! PageController::is_admin_page() ) { return $settings; } // Return early if the incentive must not be visible. if ( ! $this->must_be_visible() ) { return $settings; } $settings['wcpayWelcomePageIncentive'] = $this->get_incentive(); return $settings; } /** * Adds allowed promo notes from the WooPayments incentive. * * @param array $promo_notes Allowed promo notes. * @return array */ public function allowed_promo_notes( $promo_notes = [] ): array { // Return early if the incentive must not be visible. if ( ! $this->must_be_visible() ) { return $promo_notes; } // Add our incentive ID to the promo notes. $promo_notes[] = $this->get_incentive()['id']; return $promo_notes; } /** * Adds the WooPayments incentive badge to the onboarding task. * * @param string $badge Current badge. * * @return string */ public function onboarding_task_badge( string $badge ): string { // Return early if the incentive must not be visible. if ( ! $this->must_be_visible() ) { return $badge; } return $this->get_incentive()['task_badge'] ?? $badge; } /** * Check if the WooPayments payment gateway is active and set up or was at some point, * or there are orders processed with it, at some moment. * * @return boolean */ private function has_wcpay(): bool { // First, get the stored value, if it exists. // This way we avoid costly DB queries and API calls. // Basically, we only want to know if WooPayments was in use in the past. // Since the past can't be changed, neither can this value. $had_wcpay = get_option( self::HAD_WCPAY_OPTION_NAME ); if ( false !== $had_wcpay ) { return $had_wcpay === 'yes'; } // We need to determine the value. // Start with the assumption that the store didn't have WooPayments in use. $had_wcpay = false; // We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache. // This implies that WooPayments was active at some point and that it was connected. // If WooPayments is active right now, we will not get to this point since the plugin is active check is done first. if ( $this->has_wcpay_account_data() ) { $had_wcpay = true; } // If there is at least one order processed with WooPayments, we consider the store to have WooPayments. if ( false === $had_wcpay && ! empty( wc_get_orders( [ 'payment_method' => 'woocommerce_payments', 'return' => 'ids', 'limit' => 1, ] ) ) ) { $had_wcpay = true; } // Store the value for future use. update_option( self::HAD_WCPAY_OPTION_NAME, $had_wcpay ? 'yes' : 'no' ); return $had_wcpay; } /** * Check if the WooPayments plugin is active. * * @return boolean */ private function is_wcpay_active(): bool { return class_exists( '\WC_Payments' ); } /** * Check if there is meaningful data in the WooPayments account cache. * * @return boolean */ private function has_wcpay_account_data(): bool { $account_data = get_option( 'wcpay_account_data', [] ); if ( ! empty( $account_data['data']['account_id'] ) ) { return true; } return false; } /** * Check if the store has any paid orders. * * Currently, we look at the past 90 days and only consider orders * with status `wc-completed`, `wc-processing`, or `wc-refunded`. * * @return boolean Whether the store has any paid orders. */ private function has_orders(): bool { // First, get the stored value, if it exists. // This way we avoid costly DB queries and API calls. $has_orders = get_transient( self::HAS_ORDERS_TRANSIENT_NAME ); if ( false !== $has_orders ) { return 'yes' === $has_orders; } // We need to determine the value. // Start with the assumption that the store doesn't have orders in the timeframe we look at. $has_orders = false; // By default, we will check for new orders every 6 hours. $expiration = 6 * HOUR_IN_SECONDS; // Get the latest completed, processing, or refunded order. $latest_order = wc_get_orders( array( 'status' => array( 'wc-completed', 'wc-processing', 'wc-refunded' ), 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', ) ); if ( ! empty( $latest_order ) ) { $latest_order = reset( $latest_order ); // If the latest order is within the timeframe we look at, we consider the store to have orders. // Otherwise, it clearly doesn't have orders. if ( $latest_order instanceof WC_Abstract_Order && strtotime( $latest_order->get_date_created() ) >= strtotime( '-90 days' ) ) { $has_orders = true; // For ultimate efficiency, we will check again after 90 days from the latest order // because in all that time we will consider the store to have orders regardless of new orders. $expiration = strtotime( $latest_order->get_date_created() ) + 90 * DAY_IN_SECONDS - time(); } } // Store the value for future use. set_transient( self::HAS_ORDERS_TRANSIENT_NAME, $has_orders ? 'yes' : 'no', $expiration ); return $has_orders; } /** * Check if the current incentive has been manually dismissed. * * @return boolean */ private function is_incentive_dismissed(): bool { $dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] ); // If there are no dismissed incentives, return early. if ( empty( $dismissed_incentives ) ) { return false; } // Return early if there is no eligible incentive. $incentive = $this->get_incentive(); if ( empty( $incentive ) ) { return true; } // Search the incentive ID in the dismissed incentives list. if ( in_array( $incentive['id'], $dismissed_incentives, true ) ) { return true; } return false; } /** * Fetches and caches eligible incentive from the WooPayments API. * * @return array|null Array of eligible incentive or null. */ private function get_incentive(): ?array { // Return in-memory cached incentive if it is set. if ( isset( $this->incentive ) ) { return $this->incentive; } // Get the cached data. $cache = get_transient( self::CACHE_TRANSIENT_NAME ); // If the cached data is not expired and it's a WP_Error, // it means there was an API error previously and we should not retry just yet. if ( is_wp_error( $cache ) ) { // Initialize the in-memory cache and return it. $this->incentive = []; return $this->incentive; } // Gather the store context data. $store_context = [ // Store ISO-2 country code, e.g. `US`. 'country' => WC()->countries->get_base_country(), // Store locale, e.g. `en_US`. 'locale' => get_locale(), // WooCommerce store active for duration in seconds. 'active_for' => WCAdminHelper::get_wcadmin_active_for_in_seconds(), 'has_orders' => $this->has_orders(), // Whether the store has at least one payment gateway enabled. 'has_payments' => ! empty( WC()->payment_gateways()->get_available_payment_gateways() ), 'has_wcpay' => $this->has_wcpay(), ]; // Fingerprint the store context through a hash of certain entries. $store_context_hash = $this->generate_context_hash( $store_context ); // Use the transient cached incentive if it exists, it is not expired, // and the store context hasn't changed since we last requested from the WooPayments API (based on context hash). if ( false !== $cache && ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] ) && hash_equals( $store_context_hash, $cache['context_hash'] ) ) { // We have a store context hash and it matches with the current context one. // We can use the cached incentive data. // Store the incentive in the in-memory cache and return it. $this->incentive = $cache['incentive'] ?? []; return $this->incentive; } // By this point, we have an expired transient or the store context has changed. // Query for incentives by calling the WooPayments API. $url = add_query_arg( $store_context, 'https://public-api.wordpress.com/wpcom/v2/wcpay/incentives', ); $response = wp_remote_get( $url, [ 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ] ); // Return early if there is an error, waiting 6 hours before the next attempt. if ( is_wp_error( $response ) ) { // Store a trimmed down, lightweight error. $error = new \WP_Error( $response->get_error_code(), $response->get_error_message(), wp_remote_retrieve_response_code( $response ) ); // Store the error in the transient so we know this is due to an API error. set_transient( self::CACHE_TRANSIENT_NAME, $error, HOUR_IN_SECONDS * 6 ); // Initialize the in-memory cache and return it. $this->incentive = []; return $this->incentive; } $cache_for = wp_remote_retrieve_header( $response, 'cache-for' ); // Initialize the in-memory cache. $this->incentive = []; if ( 200 === wp_remote_retrieve_response_code( $response ) ) { // Decode the results, falling back to an empty array. $results = json_decode( wp_remote_retrieve_body( $response ), true ) ?? []; // Find all `welcome_page` incentives. $incentives = array_filter( $results, function( $incentive ) { return 'welcome_page' === $incentive['type']; } ); // Use the first found matching incentive or empty array if none was found. // Store incentive in the in-memory cache. $this->incentive = empty( $incentives ) ? [] : reset( $incentives ); } // Skip transient cache if `cache-for` header equals zero. if ( '0' === $cache_for ) { // If we have a transient cache that is not expired, delete it so there are no leftovers. if ( false !== $cache ) { delete_transient( self::CACHE_TRANSIENT_NAME ); } return $this->incentive; } // Store incentive in transient cache (together with the context hash) for the given number of seconds // or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched. set_transient( self::CACHE_TRANSIENT_NAME, [ 'incentive' => $this->incentive, 'context_hash' => $store_context_hash, 'timestamp' => time(), ], ! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS ); return $this->incentive; } /** * Generate a hash from the store context data. * * @param array $context The store context data. * * @return string The context hash. */ private function generate_context_hash( array $context ): string { // Include only certain entries in the context hash. // We need only discrete, user-interaction dependent data. // Entries like `active_for` have no place in the hash generation since they change automatically. return md5( wp_json_encode( [ 'country' => $context['country'] ?? '', 'locale' => $context['locale'] ?? '', 'has_orders' => $context['has_orders'] ?? false, 'has_payments' => $context['has_payments'] ?? false, 'has_wcpay' => $context['has_wcpay'] ?? false, ] ) ); } } CouponsMovedTrait.php 0000644 00000004233 15073235522 0010710 0 ustar 00 <?php /** * A Trait to help with managing the legacy coupon menu. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; /** * CouponsMovedTrait trait. */ trait CouponsMovedTrait { /** * The GET query key for the legacy menu. * * @var string */ protected static $query_key = 'legacy_coupon_menu'; /** * The key for storing an option in the DB. * * @var string */ protected static $option_key = 'wc_admin_show_legacy_coupon_menu'; /** * Get the URL for the legacy coupon management. * * @return string The unescaped URL for the legacy coupon management page. */ protected static function get_legacy_coupon_url() { return self::get_coupon_url( [ self::$query_key => true ] ); } /** * Get the URL for the coupon management page. * * @param array $args Additional URL query arguments. * * @return string */ protected static function get_coupon_url( $args = [] ) { $args = array_merge( [ 'post_type' => 'shop_coupon', ], $args ); return add_query_arg( $args, admin_url( 'edit.php' ) ); } /** * Get the new URL for managing coupons. * * @param string $page The management page. * * @return string */ protected static function get_management_url( $page ) { $path = ''; switch ( $page ) { case 'coupon': case 'coupons': return self::get_coupon_url(); case 'marketing': $path = self::get_marketing_path(); break; } return "wc-admin&path={$path}"; } /** * Get the WC Admin path for the marking page. * * @return string */ protected static function get_marketing_path() { return '/marketing/overview'; } /** * Whether we should display the legacy coupon menu item. * * @return bool */ protected static function should_display_legacy_menu() { return ( get_option( self::$option_key, 1 ) && ! Features::is_enabled( 'navigation' ) ); } /** * Set whether we should display the legacy coupon menu item. * * @param bool $display Whether the menu should be displayed or not. */ protected static function display_legacy_menu( $display = false ) { update_option( self::$option_key, $display ? 1 : 0 ); } } SiteHealth.php 0000644 00000004502 15073235522 0007314 0 ustar 00 <?php /** * Customize Site Health recommendations for WooCommerce. */ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; /** * SiteHealth class. */ class SiteHealth { /** * Class instance. * * @var SiteHealth instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { add_filter( 'site_status_should_suggest_persistent_object_cache', array( $this, 'should_suggest_persistent_object_cache' ) ); } /** * Counts specific types of WooCommerce entities to determine if a persistent object cache would be beneficial. * * Note that if all measured WooCommerce entities are below their thresholds, this will return null so that the * other normal WordPress checks will still be run. * * @param true|null $check A non-null value will short-circuit WP's normal tests for this. * * @return true|null True if the store would benefit from a persistent object cache. Otherwise null. */ public function should_suggest_persistent_object_cache( $check ) { // Skip this if some other filter has already determined yes. if ( true === $check ) { return $check; } $thresholds = array( 'orders' => 100, 'products' => 100, ); foreach ( $thresholds as $key => $threshold ) { try { switch ( $key ) { case 'orders': $orders_query = new \WC_Order_Query( array( 'status' => 'any', 'limit' => 1, 'paginate' => true, 'return' => 'ids', ) ); $orders_results = $orders_query->get_orders(); if ( $orders_results->total >= $threshold ) { $check = true; } break; case 'products': $products_query = new \WC_Product_Query( array( 'status' => 'any', 'limit' => 1, 'paginate' => true, 'return' => 'ids', ) ); $products_results = $products_query->get_products(); if ( $products_results->total >= $threshold ) { $check = true; } break; } } catch ( \Exception $exception ) { break; } if ( ! is_null( $check ) ) { break; } } return $check; } } WCAdminAssets.php 0000644 00000036407 15073235522 0007740 0 ustar 00 <?php /** * Register the scripts, and styles used within WooCommerce Admin. */ namespace Automattic\WooCommerce\Internal\Admin; use _WP_Dependency; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Internal\Admin\Loader; /** * WCAdminAssets Class. */ class WCAdminAssets { /** * Class instance. * * @var WCAdminAssets instance */ protected static $instance = null; /** * An array of dependencies that have been preloaded (to avoid duplicates). * * @var array */ protected $preloaded_dependencies; /** * 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(); add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'inject_wc_settings_dependencies' ), 14 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ), 15 ); } /** * Gets the path for the asset depending on file type. * * @param string $ext File extension. * @return string Folder path of asset. */ public static function get_path( $ext ) { return ( $ext === 'css' ) ? WC_ADMIN_DIST_CSS_FOLDER : WC_ADMIN_DIST_JS_FOLDER; } /** * Determines if a minified JS file should be served. * * @param boolean $script_debug Only serve unminified files if script debug is on. * @return boolean If js asset should use minified version. */ public static function should_use_minified_js_file( $script_debug ) { // minified files are only shipped in non-core versions of wc-admin, return false if minified files are not available. if ( ! Features::exists( 'minified-js' ) ) { return false; } // Otherwise we will serve un-minified files if SCRIPT_DEBUG is on, or if anything truthy is passed in-lieu of SCRIPT_DEBUG. return ! $script_debug; } /** * Gets the URL to an asset file. * * @param string $file File name (without extension). * @param string $ext File extension. * @return string URL to asset. */ public static function get_url( $file, $ext ) { $suffix = ''; // Potentially enqueue minified JavaScript. if ( $ext === 'js' ) { $script_debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG; $suffix = self::should_use_minified_js_file( $script_debug ) ? '.min' : ''; } return plugins_url( self::get_path( $ext ) . $file . $suffix . '.' . $ext, WC_ADMIN_PLUGIN_FILE ); } /** * Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise. * * @param string $ext File extension. * @return string The cache buster value to use for the given file. */ public static function get_file_version( $ext ) { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { return filemtime( WC_ADMIN_ABSPATH . self::get_path( $ext ) ); } return WC_VERSION; } /** * Gets a script asset registry filename. The asset registry lists dependencies for the given script. * * @param string $script_path_name Path to where the script asset registry is contained. * @param string $file File name (without extension). * @return string complete asset filename. * * @throws \Exception Throws an exception when a readable asset registry file cannot be found. */ public static function get_script_asset_filename( $script_path_name, $file ) { $minification_supported = Features::exists( 'minified-js' ); $script_min_filename = $file . '.min.asset.php'; $script_nonmin_filename = $file . '.asset.php'; $script_asset_path = WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/'; // Check minification is supported first, to avoid multiple is_readable checks when minification is // not supported. if ( $minification_supported && is_readable( $script_asset_path . $script_min_filename ) ) { return $script_min_filename; } elseif ( is_readable( $script_asset_path . $script_nonmin_filename ) ) { return $script_nonmin_filename; } else { // could not find an asset file, throw an error. throw new \Exception( 'Could not find asset registry for ' . $script_path_name ); } } /** * Render a preload link tag for a dependency, optionally * checked against a provided allowlist. * * See: https://macarthur.me/posts/preloading-javascript-in-wordpress * * @param WP_Dependency $dependency The WP_Dependency being preloaded. * @param string $type Dependency type - 'script' or 'style'. * @param array $allowlist Optional. List of allowed dependency handles. */ private function maybe_output_preload_link_tag( $dependency, $type, $allowlist = array() ) { if ( ( ! empty( $allowlist ) && ! in_array( $dependency->handle, $allowlist, true ) ) || ( ! empty( $this->preloaded_dependencies[ $type ] ) && in_array( $dependency->handle, $this->preloaded_dependencies[ $type ], true ) ) ) { return; } $this->preloaded_dependencies[ $type ][] = $dependency->handle; $source = $dependency->ver ? add_query_arg( 'ver', $dependency->ver, $dependency->src ) : $dependency->src; echo '<link rel="preload" href="', esc_url( $source ), '" as="', esc_attr( $type ), '" />', "\n"; } /** * Output a preload link tag for dependencies (and their sub dependencies) * with an optional allowlist. * * See: https://macarthur.me/posts/preloading-javascript-in-wordpress * * @param string $type Dependency type - 'script' or 'style'. * @param array $allowlist Optional. List of allowed dependency handles. */ private function output_header_preload_tags_for_type( $type, $allowlist = array() ) { if ( $type === 'script' ) { $dependencies_of_type = wp_scripts(); } elseif ( $type === 'style' ) { $dependencies_of_type = wp_styles(); } else { return; } foreach ( $dependencies_of_type->queue as $dependency_handle ) { $dependency = $dependencies_of_type->query( $dependency_handle, 'registered' ); if ( $dependency === false ) { continue; } // Preload the subdependencies first. foreach ( $dependency->deps as $sub_dependency_handle ) { $sub_dependency = $dependencies_of_type->query( $sub_dependency_handle, 'registered' ); if ( $sub_dependency ) { $this->maybe_output_preload_link_tag( $sub_dependency, $type, $allowlist ); } } $this->maybe_output_preload_link_tag( $dependency, $type, $allowlist ); } } /** * Output preload link tags for all enqueued stylesheets and scripts. * * See: https://macarthur.me/posts/preloading-javascript-in-wordpress */ private function output_header_preload_tags() { $wc_admin_scripts = array( WC_ADMIN_APP, 'wc-components', ); $wc_admin_styles = array( WC_ADMIN_APP, 'wc-components', 'wc-material-icons', ); // Preload styles. $this->output_header_preload_tags_for_type( 'style', $wc_admin_styles ); // Preload scripts. $this->output_header_preload_tags_for_type( 'script', $wc_admin_scripts ); } /** * Loads the required scripts on the correct pages. */ public function enqueue_assets() { if ( ! PageController::is_admin_or_embed_page() ) { return; } wp_enqueue_script( WC_ADMIN_APP ); wp_enqueue_style( WC_ADMIN_APP ); wp_enqueue_style( 'wc-material-icons' ); wp_enqueue_style( 'wc-onboarding' ); // Preload our assets. $this->output_header_preload_tags(); } /** * Registers all the necessary scripts and styles to show the admin experience. */ public function register_scripts() { if ( ! function_exists( 'wp_set_script_translations' ) ) { return; } $js_file_version = self::get_file_version( 'js' ); $css_file_version = self::get_file_version( 'css' ); $scripts = array( 'wc-admin-layout', 'wc-explat', 'wc-experimental', 'wc-customer-effort-score', // NOTE: This should be removed when Gutenberg is updated and the notices package is removed from WooCommerce Admin. 'wc-notices', 'wc-number', 'wc-tracks', 'wc-date', 'wc-components', WC_ADMIN_APP, 'wc-csv', 'wc-store-data', 'wc-currency', 'wc-navigation', 'wc-block-templates', 'wc-product-editor', ); $scripts_map = array( WC_ADMIN_APP => 'app', 'wc-csv' => 'csv-export', 'wc-store-data' => 'data', ); $translated_scripts = array( 'wc-currency', 'wc-date', 'wc-components', 'wc-customer-effort-score', 'wc-experimental', WC_ADMIN_APP, ); foreach ( $scripts as $script ) { $script_path_name = isset( $scripts_map[ $script ] ) ? $scripts_map[ $script ] : str_replace( 'wc-', '', $script ); try { $script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' ); $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename; global $wp_version; if ( 'app' === $script_path_name && version_compare( $wp_version, '6.3', '<' ) ) { // Remove wp-router dependency for WordPress versions < 6.3 because wp-router is not included in those versions. We only use wp-router in customize store pages and the feature is only available in WordPress 6.3+. // We can remove this once our minimum support is WP 6.3. $script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-router' ) ); } // Remove wp-editor dependency if we're not on a customize store page since we don't use wp-editor in other pages. $is_customize_store_page = ( PageController::is_admin_page() && isset( $_GET['path'] ) && str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' ) ); if ( ! $is_customize_store_page && WC_ADMIN_APP === $script ) { $script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-editor' ) ); } wp_register_script( $script, self::get_url( $script_path_name . '/index', 'js' ), $script_assets ['dependencies'], $js_file_version, true ); if ( in_array( $script, $translated_scripts, true ) ) { wp_set_script_translations( $script, 'woocommerce' ); } } catch ( \Exception $e ) { // Avoid crashing WordPress if an asset file could not be loaded. wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__, $script_path_name ); } } wp_register_style( 'wc-admin-layout', self::get_url( 'admin-layout/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-admin-layout', 'rtl', 'replace' ); wp_register_style( 'wc-components', self::get_url( 'components/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-components', 'rtl', 'replace' ); wp_register_style( 'wc-block-templates', self::get_url( 'block-templates/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-block-templates', 'rtl', 'replace' ); wp_register_style( 'wc-product-editor', self::get_url( 'product-editor/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-product-editor', 'rtl', 'replace' ); wp_register_style( 'wc-customer-effort-score', self::get_url( 'customer-effort-score/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-customer-effort-score', 'rtl', 'replace' ); wp_register_style( 'wc-experimental', self::get_url( 'experimental/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-experimental', 'rtl', 'replace' ); wp_localize_script( WC_ADMIN_APP, 'wcAdminAssets', array( 'path' => plugins_url( self::get_path( 'js' ), WC_ADMIN_PLUGIN_FILE ), 'version' => $js_file_version, ) ); wp_register_style( WC_ADMIN_APP, self::get_url( 'app/style', 'css' ), array( 'wc-components', 'wc-admin-layout', 'wc-customer-effort-score', 'wc-product-editor', 'wp-components', 'wc-experimental' ), $css_file_version ); wp_style_add_data( WC_ADMIN_APP, 'rtl', 'replace' ); wp_register_style( 'wc-onboarding', self::get_url( 'onboarding/style', 'css' ), array(), $css_file_version ); wp_style_add_data( 'wc-onboarding', 'rtl', 'replace' ); } /** * Injects wp-shared-settings as a dependency if it's present. */ public function inject_wc_settings_dependencies() { $wp_scripts = wp_scripts(); if ( wp_script_is( 'wc-settings', 'registered' ) ) { $handles_for_injection = array( 'wc-admin-layout', 'wc-csv', 'wc-currency', 'wc-customer-effort-score', 'wc-navigation', // NOTE: This should be removed when Gutenberg is updated and // the notices package is removed from WooCommerce Admin. 'wc-notices', 'wc-number', 'wc-date', 'wc-components', 'wc-tracks', 'wc-block-templates', 'wc-product-editor', ); foreach ( $handles_for_injection as $handle ) { $script = $wp_scripts->query( $handle, 'registered' ); if ( $script instanceof _WP_Dependency ) { $script->deps[] = 'wc-settings'; $wp_scripts->add_data( $handle, 'group', 1 ); } } foreach ( $wp_scripts->registered as $handle => $script ) { // scripts that are loaded in the footer has extra->group = 1. if ( array_intersect( $handles_for_injection, $script->deps ) && ! isset( $script->extra['group'] ) ) { // Append the script to footer. $wp_scripts->add_data( $handle, 'group', 1 ); // Show a warning. $error_handle = 'wc-settings-dep-in-header'; $used_deps = implode( ', ', array_intersect( $handles_for_injection, $script->deps ) ); $error_message = "Scripts that have a dependency on [$used_deps] must be loaded in the footer, {$handle} was registered to load in the header, but has been switched to load in the footer instead. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5059"; // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( $error_handle, '' ); wp_enqueue_script( $error_handle ); wp_add_inline_script( $error_handle, sprintf( 'console.warn( "%s" );', $error_message ) ); } } } } /** * Loads a script * * @param string $script_path_name The script path name. * @param string $script_name Filename of the script to load. * @param bool $need_translation Whether the script need translations. * @param array $dependencies Array of any extra dependencies. Note wc-admin and any application JS dependencies are automatically added by Dependency Extraction Webpack Plugin. Use this parameter to designate any extra dependencies. */ public static function register_script( $script_path_name, $script_name, $need_translation = false, $dependencies = array() ) { $script_assets_filename = self::get_script_asset_filename( $script_path_name, $script_name ); $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename; wp_enqueue_script( 'wc-admin-' . $script_name, self::get_url( $script_path_name . '/' . $script_name, 'js' ), array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'], $dependencies ), self::get_file_version( 'js' ), true ); if ( $need_translation ) { wp_set_script_translations( 'wc-admin-' . $script_name, 'woocommerce' ); } } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings