SystemStatusReport.php000064400000013537150732355220011156 0ustar00 render_features(); $this->render_daily_cron(); $this->render_options(); $this->render_notes(); $this->render_onboarding_state(); ?>

: : : ' . esc_html__( 'Not scheduled', 'woocommerce' ) . ''; } else { echo ' Next scheduled: ' . esc_html( date_i18n( 'Y-m-d H:i:s P', $next_daily_cron ) ) . ''; } ?> : '; } else { echo ' ' . esc_html__( 'Not all expected', 'woocommerce' ) . ''; } ?> : : 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.php000064400000003006150732355220011412 0ustar00on_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.php000064400000001535150732355220012534 0ustar00base ) && '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.php000064400000052471150732355220011703 0ustar00 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 ) ? '

' . implode( "
\n", $messages ) . '

' : ''; // 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 ) . ' ' . __( 'Undo', 'woocommerce' ) . '
'; } 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 ) . ' ' . __( 'Undo', 'woocommerce' ) . '
'; } 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 ' ' . esc_html( $count ) . ''; } /** * 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(); ?>

reviews_list_table->views(); ?>
reviews_list_table->search_box( __( 'Search Reviews', 'woocommerce' ), 'reviews' ); ?> reviews_list_table->display(); ?>
reviews_list_table ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } ProductReviews/ReviewsListTable.php000064400000133171150732355220013504 0ustar00 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' ); ?> print_column_headers(); ?> display_rows_or_placeholder(); ?> print_column_headers( false ); ?>
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 ); ?> single_row_columns( $comment ); ?> 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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( '%s', 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 = ''; $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 = '
'; $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 .= "$sep$link"; } $output .= '
'; $output .= ''; return $output; } /** * Gets the columns for the table. * * @return array Table columns and their headings. */ public function get_columns() : array { $columns = [ 'cb' => '', '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 (%s)', 'All (%s)', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'moderated' => _nx_noop( 'Pending (%s)', 'Pending (%s)', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'approved' => _nx_noop( 'Approved (%s)', 'Approved (%s)', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'spam' => _nx_noop( 'Spam (%s)', 'Spam (%s)', 'product reviews', 'woocommerce' ), /* translators: %s: Number of reviews. */ 'trash' => _nx_noop( 'Trash (%s)', 'Trash (%s)', '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( '%s', ( 'moderated' === $status ) ? 'pending' : $status, number_format_i18n( $number_reviews_for_status ) ); $status_links[ $status ] = '' . sprintf( translate_nooped_plural( $label, $number_reviews_for_status ), $count_html ) . ''; } 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 ) { ?> 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 . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } printf( '%1$s%2$s%3$s', '
', get_comment_text( $item->comment_ID ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped '
' ); if ( $this->current_user_can_edit_review ) { ?> 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' ) ), '' . esc_html( $review_author_name ) . '' ); } /** * 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 '' . $author_avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped comment_author(); echo '
'; if ( ! empty( $author_url ) ) : ?>
current_user_can_edit_review ) : if ( ! empty( $item->comment_author_email ) && is_email( $item->comment_author_email ) ) : ?> comment_author_email ); ?>
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; ?> comment_ID ); ?> 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(); ?>
comment_post_ID ) ) : printf( '%2$s', esc_url( get_comment_link( $item ) ), esc_html( $submitted ) ); else : echo esc_html( $submitted ); endif; ?>
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 ) : ?> 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 ); ?> 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 '
'; if ( 'top' === $which ) { ob_start(); echo ''; $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 '
'; } /** * Displays a review type drop-down for filtering reviews in the Product Reviews list table. * * @see WP_Comments_List_Table::comment_type_dropdown() for consistency. * * @param string|mixed $current_type The current comment item type slug. * @return void */ protected function review_type_dropdown( $current_type ) : void { /** * Sets the possible options used in the Product Reviews List Table's filter-by-review-type * selector. * * @since 7.0.0 * * @param array Map of possible review types. */ $item_types = apply_filters( 'woocommerce_product_reviews_list_table_item_types', array( 'all' => __( 'All types', 'woocommerce' ), 'comment' => __( 'Replies', 'woocommerce' ), 'review' => __( 'Reviews', 'woocommerce' ), ) ); ?> __( 'All ratings', 'woocommerce' ), '1' => '★', '2' => '★★', '3' => '★★★', '4' => '★★★★', '5' => '★★★★★', ]; ?>
%s', esc_html__( 'No reviews', 'woocommerce' ) ); } elseif ( $approved_review_count && 'trash' === get_post_status( $post_id ) ) { // Don't link the comment bubble for a trashed product. printf( '%s', esc_html( $approved_reviews_number ), $pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase ) ); } elseif ( $approved_review_count ) { // Link the comment bubble to approved reviews. printf( '%s', esc_url( add_query_arg( [ 'product_id' => urlencode( $post_id ), 'comment_status' => 'approved', ], Reviews::get_reviews_page_url() ) ), esc_html( $approved_reviews_number ), $pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase ) ); } else { // Don't link the comment bubble when there are no approved reviews. printf( '%s', esc_html( $approved_reviews_number ), $pending_comments ? esc_html__( 'No approved reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' ) ); } if ( $pending_comments ) { printf( '%s', esc_url( add_query_arg( [ 'product_id' => urlencode( $post_id ), 'comment_status' => 'moderated', ], Reviews::get_reviews_page_url() ) ), esc_html( $pending_reviews_number ), esc_html( $pending_phrase ) ); } else { printf( '%s', esc_html( $pending_reviews_number ), $approved_review_count ? esc_html__( 'No pending reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' ) ); } } } ProductReviews/ReviewsCommentsOverrides.php000064400000011354150732355220015267 0ustar00base !== 'edit-comments' ) { return; } $this->maybe_display_reviews_moved_notice(); } /** * May render an admin notice informing the user that reviews were moved to a new page. * * @return void */ protected function maybe_display_reviews_moved_notice() : void { if ( $this->should_display_reviews_moved_notice() ) { $this->display_reviews_moved_notice(); } } /** * Checks if the admin notice informing the user that reviews were moved to a new page should be displayed. * * @return bool */ protected function should_display_reviews_moved_notice() : bool { // Do not display if the user does not have the capability to see the new page. if ( ! WC()->call_function( 'current_user_can', Reviews::get_capability() ) ) { return false; } // Do not display if the current user has dismissed this notice. if ( WC()->call_function( 'get_user_meta', get_current_user_id(), 'dismissed_' . static::REVIEWS_MOVED_NOTICE_ID . '_notice', true ) ) { return false; } return true; } /** * Renders an admin notice informing the user that reviews were moved to a new page. * * @return void */ protected function display_reviews_moved_notice() : void { ?>

Reviews.', 'woocommerce' ); ?>

id ) { return $args; } if ( ! empty( $args['post_type'] ) && $args['post_type'] !== 'any' ) { $post_types = (array) $args['post_type']; } else { $post_types = get_post_types(); } $index = array_search( 'product', $post_types ); if ( $index !== false ) { unset( $post_types[ $index ] ); } if ( ! is_array( $args ) ) { $args = []; } $args['post_type'] = $post_types; return $args; } } Marketplace.php000064400000011550150732355220007513 0ustar00get( FeaturesController::class ); $feature_controller->change_feature_enable( 'marketplace', true ); } add_action( 'admin_menu', array( $this, 'register_pages' ), 70 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); // Add a Woo Marketplace link to the plugin install action links. add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) ); add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) ); add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) ); } /** * Registers report pages. */ public function register_pages() { if ( ! function_exists( 'wc_admin_register_page' ) ) { return; } $marketplace_pages = self::get_marketplace_pages(); foreach ( $marketplace_pages as $marketplace_page ) { if ( ! is_null( $marketplace_page ) ) { wc_admin_register_page( $marketplace_page ); } } } /** * Get report pages. */ public static function get_marketplace_pages() { $marketplace_pages = array( array( 'id' => 'woocommerce-marketplace', 'parent' => 'woocommerce', 'title' => __( 'Extensions', 'woocommerce' ), 'path' => '/extensions', ), ); /** * The marketplace items used in the menu. * * @since 8.0 */ return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages ); } /** * Enqueue update script. * * @param string $hook_suffix The current admin page. */ public function enqueue_scripts( $hook_suffix ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( 'woocommerce_page_wc-admin' !== $hook_suffix ) { return; }; if ( ! isset( $_GET['path'] ) || '/extensions' !== $_GET['path'] ) { return; } // Enqueue WordPress updates script to enable plugin and theme installs and updates. wp_enqueue_script( 'updates' ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Add a Woo Marketplace link to the plugin install action links. * * @param array $tabs Plugins list tabs. * @return array */ public function add_woo_plugin_install_action_link( $tabs ) { $tabs[ self::MARKETPLACE_TAB_SLUG ] = 'Woo'; return $tabs; } /** * Open the Woo tab when the user clicks on the Woo link in the plugin installer. */ public function maybe_open_woo_tab() { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['tab'] ) || self::MARKETPLACE_TAB_SLUG !== $_GET['tab'] ) { return; } // phpcs:enable WordPress.Security.NonceVerification.Recommended $woo_url = add_query_arg( array( 'page' => 'wc-admin', 'path' => '/extensions', 'tab' => 'extensions', 'ref' => 'plugins', ), admin_url( 'admin.php' ) ); wp_safe_redirect( $woo_url ); exit; } /** * Add styles to the plugin install page. */ public function add_plugins_page_styles() { ?> Coupons. */ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved; use Automattic\WooCommerce\Admin\PageController; /** * Contains backend logic for the Coupons feature. */ class Coupons { use CouponsMovedTrait; /** * Class instance. * * @var Coupons instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook into WooCommerce. */ public function __construct() { if ( ! is_admin() ) { return; } // If the main marketing feature is disabled, don't modify coupon behavior. if ( ! Features::is_enabled( 'marketing' ) ) { return; } // Only support coupon modifications if coupons are enabled. if ( ! wc_coupons_enabled() ) { return; } ( new CouponPageMoved() )->init(); add_action( 'admin_enqueue_scripts', array( $this, 'maybe_add_marketing_coupon_script' ) ); add_action( 'woocommerce_register_post_type_shop_coupon', array( $this, 'move_coupons' ) ); add_action( 'admin_head', array( $this, 'fix_coupon_menu_highlight' ), 99 ); add_action( 'admin_menu', array( $this, 'maybe_add_coupon_menu_redirect' ) ); } /** * Maybe add menu item back in original spot to help people transition */ public function maybe_add_coupon_menu_redirect() { if ( ! $this->should_display_legacy_menu() ) { return; } add_submenu_page( 'woocommerce', __( 'Coupons', 'woocommerce' ), __( 'Coupons', 'woocommerce' ), 'manage_options', 'coupons-moved', [ $this, 'coupon_menu_moved' ] ); } /** * Call back for transition menu item */ public function coupon_menu_moved() { wp_safe_redirect( $this->get_legacy_coupon_url(), 301 ); exit(); } /** * Modify registered post type shop_coupon * * @param array $args Array of post type parameters. * * @return array the filtered parameters. */ public function move_coupons( $args ) { $args['show_in_menu'] = current_user_can( 'manage_woocommerce' ) ? 'woocommerce-marketing' : true; return $args; } /** * Undo WC modifications to $parent_file for 'shop_coupon' */ public function fix_coupon_menu_highlight() { global $parent_file, $post_type; if ( $post_type === 'shop_coupon' ) { $parent_file = 'woocommerce-marketing'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride } } /** * Maybe add our wc-admin coupon scripts if viewing coupon pages */ public function maybe_add_marketing_coupon_script() { $curent_screen = PageController::get_instance()->get_current_page(); if ( ! isset( $curent_screen['id'] ) || $curent_screen['id'] !== 'woocommerce-coupons' ) { return; } $rtl = is_rtl() ? '-rtl' : ''; wp_enqueue_style( 'wc-admin-marketing-coupons', WCAdminAssets::get_url( "marketing-coupons/style{$rtl}", 'css' ), array(), WCAdminAssets::get_file_version( 'css' ) ); WCAdminAssets::register_script( 'wp-admin-scripts', 'marketing-coupons', true ); } } Marketing.php000064400000007404150732355220007207 0ustar00connect_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.php000064400000021275150732355220007360 0ustar00=' ) ) { // 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.php000064400000022206150732355220011471 0ustar00allowed_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.php000064400000003002150732355220012205 0ustar00set_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.php000064400000006601150732355220013617 0ustar00get_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.php000064400000004175150732355220011010 0ustar00 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.php000064400000004205150732355220013521 0ustar00 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.php000064400000003260150732355220011416 0ustar00set_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.php000064400000003572150732355220013367 0ustar00set_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.php000064400000014370150732355220012316 0ustar00countries->get_base_country() ) { return; } $data_store = Notes::load_data_store(); // We already have this note? Then mark the note as actioned. $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_ids ) ) { $note_id = array_pop( $note_ids ); $note = Notes::get_note( $note_id ); if ( false === $note ) { return; } // If the WooCommerce Payments plugin was installed after the note was created, make sure it's marked as actioned. if ( self::is_installed() && Note::E_WC_ADMIN_NOTE_ACTIONED !== $note->get_status() ) { $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } return; } $current_date = new \DateTime(); $publish_date = new \DateTime( '2020-04-14' ); if ( $current_date >= $publish_date ) { $note = self::get_note(); if ( self::can_be_added() ) { $note->save(); } return; } else { $hook_name = sprintf( '%s_add_note', self::NOTE_NAME ); if ( ! WC()->queue()->get_next( $hook_name ) ) { WC()->queue()->schedule_single( $publish_date->getTimestamp(), $hook_name ); } } } /** * Add a note about WooCommerce Payments. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Try the new way to get paid', 'woocommerce' ) ); $note->set_content( __( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with WooPayments.', 'woocommerce' ) . '

' . sprintf( /* translators: 1: opening link tag, 2: closing tag */ __( 'By clicking "Get started", you agree to our %1$sTerms of Service%2$s', 'woocommerce' ), '', '' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/payments/?utm_medium=product', Note::E_WC_ADMIN_NOTE_UNACTIONED ); $note->add_action( 'get-started', __( 'Get started', 'woocommerce' ), wc_admin_url( '&action=setup-woocommerce-payments' ), Note::E_WC_ADMIN_NOTE_ACTIONED, true ); $note->add_nonce_to_action( 'get-started', 'setup-woocommerce-payments', '' ); // Create the note as "actioned" if the plugin is already installed. if ( self::is_installed() ) { $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); } return $note; } /** * Check if the WooCommerce Payments plugin is active or installed. */ protected static function is_installed() { if ( defined( 'WC_Payments' ) ) { return true; } include_once ABSPATH . '/wp-admin/includes/plugin.php'; return 0 === validate_plugin( self::PLUGIN_FILE ); } /** * Install and activate WooCommerce Payments. * * @return boolean Whether the plugin was successfully activated. */ private function install_and_activate_wcpay() { $install_request = array( 'plugins' => self::PLUGIN_SLUG ); $installer = new \Automattic\WooCommerce\Admin\API\Plugins(); $result = $installer->install_plugins( $install_request ); if ( is_wp_error( $result ) ) { return false; } wc_admin_record_tracks_event( 'woocommerce_payments_install', array( 'context' => 'inbox' ) ); $activate_request = array( 'plugins' => self::PLUGIN_SLUG ); $result = $installer->activate_plugins( $activate_request ); if ( is_wp_error( $result ) ) { return false; } return true; } /** * Install & activate WooCommerce Payments plugin, and redirect to setup. */ public function install_on_action() { // TODO: Need to validate this request more strictly since we're taking install actions directly? if ( ! isset( $_GET['page'] ) || 'wc-admin' !== $_GET['page'] || ! isset( $_GET['action'] ) || 'setup-woocommerce-payments' !== $_GET['action'] ) { return; } $data_store = Notes::load_data_store(); // We already have this note? Then mark the note as actioned. $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( empty( $note_ids ) ) { return; } $note_id = array_pop( $note_ids ); $note = Notes::get_note( $note_id ); if ( false === $note ) { return; } $action = $note->get_action( 'get-started' ); if ( ! $action || ( isset( $action->nonce_action ) && ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), $action->nonce_action ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput ) ) ) { return; } if ( ! current_user_can( 'install_plugins' ) ) { return; } $this->install_and_activate_wcpay(); // WooCommerce Payments is installed at this point, so link straight into the onboarding flow. $connect_url = add_query_arg( array( 'wcpay-connect' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), ), admin_url() ); wp_safe_redirect( $connect_url ); exit; } } Notes/TrackingOptIn.php000064400000005355150732355220011075 0ustar00', '', '' ); $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.php000064400000007255150732355220011605 0ustar00get_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.php000064400000003676150732355220012576 0ustar00must_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.php000064400000003205150732355220012237 0ustar00set_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.php000064400000004000150732355220012707 0ustar00must_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.php000064400000034346150732355220012730 0ustar00remove_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.php000064400000012404150732355220011232 0ustar00 $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.php000064400000011122150732355220012550 0ustar00set_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.php000064400000003202150732355220010403 0ustar00countries->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.php000064400000004320150732355220012126 0ustar00set_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.php000064400000004570150732355220012465 0ustar00set_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.php000064400000004156150732355220012473 0ustar00set_title( __( 'Potentially unsecured files were found in your uploads directory', 'woocommerce' ) ); $note->set_content( sprintf( /* translators: 1: opening analytics docs link tag. 2: closing link tag */ __( 'Files that may contain %1$sstore analytics%2$s reports were found in your uploads directory - we recommend assessing and deleting any such files.', 'woocommerce' ), '', '' ) ); $note->set_content_data( (object) array() ); $note->set_type( Note::E_WC_ADMIN_NOTE_ERROR ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://developer.woo.com/2021/09/22/important-security-patch-released-in-woocommerce/', Note::E_WC_ADMIN_NOTE_UNACTIONED, true ); $note->add_action( 'dismiss', __( 'Dismiss', 'woocommerce' ), wc_admin_url(), Note::E_WC_ADMIN_NOTE_ACTIONED, false ); return $note; } /** * Add the note if it passes predefined conditions. */ public static function possibly_add_note() { $note = self::get_note(); if ( self::note_exists() ) { return; } $note->save(); } /** * Check if the note has been previously added. */ public static function note_exists() { $data_store = \WC_Data_Store::load( 'admin-note' ); $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); return ! empty( $note_ids ); } } Notes/PersonalizeStore.php000064400000003646150732355220011672 0ustar00set_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.php000064400000005435150732355220011421 0ustar00 1, 'return' => 'ids', 'status' => array( 'publish' ), ) ); $products = $query->get_products(); if ( 0 !== count( $products ) ) { return; } // Don't show if there is an orders. $args = array( 'limit' => 1, 'return' => 'ids', ); $orders = wc_get_orders( $args ); if ( 0 !== count( $orders ) ) { return; } // If you're updating the following please use sprintf to separate HTML tags. // https://github.com/woocommerce/woocommerce-admin/pull/6617#discussion_r596889685. $content_lines = array( '{greetings}

', /* translators: %s: line break */ sprintf( __( 'Nice one; you\'ve created a WooCommerce store! Now it\'s time to add your first product and get ready to start selling.%s', 'woocommerce' ), '

' ), __( 'There are three ways to add your products: you can create products manually, import them at once via CSV file, or migrate them from another service.

', 'woocommerce' ), /* translators: %1$s is an open anchor tag () and %2$s is a close link tag (). */ sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '', '' ), ); $additional_data = array( 'role' => 'administrator', ); $note = new Note(); $note->set_title( __( 'Add your first product', 'woocommerce' ) ); $note->set_content( implode( '', $content_lines ) ); $note->set_content_data( (object) $additional_data ); $note->set_image( plugins_url( '/images/admin_notes/dashboard-widget-setup.png', WC_ADMIN_PLUGIN_FILE ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_EMAIL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-admin' ); $note->add_action( 'add-first-product', __( 'Add a product', 'woocommerce' ), admin_url( 'admin.php?page=wc-admin&task=products' ) ); return $note; } } Notes/EmailNotification.php000064400000012250150732355220011747 0ustar00note = $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.php000064400000003354150732355220012161 0ustar00payment_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.php000064400000004533150732355220013347 0ustar00 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.php000064400000002605150732355220010224 0ustar00set_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.php000064400000007463150732355220011416 0ustar00 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.php000064400000003041150732355220012142 0ustar00set_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.php000064400000002676150732355220011120 0ustar00set_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.php000064400000005261150732355220012306 0ustar00set_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.php000064400000004704150732355220011622 0ustar00queue()->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.php000064400000003002150732355220012215 0ustar00set_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.php000064400000003254150732355220012553 0ustar00set_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.php000064400000007160150732355220013130 0ustar00logger = $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.php000064400000020647150732355220012470 0ustar00 \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.php000064400000007055150732355220013214 0ustar00 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.php000064400000011457150732355220012503 0ustar00search( 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.php000064400000001306150732355220012455 0ustar00 'obw/basics', 'title' => __( 'Get the basics', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'woocommerce-payments' ), self::get_plugin( 'woocommerce-services:shipping' ), self::get_plugin( 'woocommerce-services:tax' ), self::get_plugin( 'jetpack' ), ), ), array( 'key' => 'obw/grow', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'mailpoet' ), self::get_plugin( 'google-listings-and-ads' ), self::get_plugin( 'pinterest-for-woocommerce' ), self::get_plugin( 'facebook-for-woocommerce' ), ), ), array( 'key' => 'task-list/reach', 'title' => __( 'Reach out to customers', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'mailpoet:alt' ), self::get_plugin( 'mailchimp-for-woocommerce' ), self::get_plugin( 'klaviyo' ), ), ), array( 'key' => 'task-list/grow', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => array( self::get_plugin( 'google-listings-and-ads:alt' ), self::get_plugin( 'tiktok-for-business' ), self::get_plugin( 'pinterest-for-woocommerce:alt' ), self::get_plugin( 'facebook-for-woocommerce:alt' ), ), ), array( 'key' => 'obw/core-profiler', 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => self::with_core_profiler_fields( array( self::get_plugin( 'woocommerce-payments' ), self::get_plugin( 'woocommerce-services:shipping' ), self::get_plugin( 'jetpack' ), self::get_plugin( 'pinterest-for-woocommerce' ), self::get_plugin( 'mailpoet' ), self::get_plugin( 'google-listings-and-ads' ), self::get_plugin( 'woocommerce-services:tax' ), self::get_plugin( 'tiktok-for-business' ), ) ), ), ); $bundles = wp_json_encode( $bundles ); return json_decode( $bundles ); } /** * Get the plugin arguments by slug. * * @param string $slug Slug. * @return array */ public static function get_plugin( $slug ) { $plugins = array( 'google-listings-and-ads' => array( 'min_php_version' => '7.4', 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ), '', '' ), 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'google-listings-and-ads' ), ), ), ), ), ), 'google-listings-and-ads:alt' => array( 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, ), 'facebook-for-woocommerce' => array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'description' => __( 'List products and create ads on Facebook and Instagram with Facebook for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-facebook', 'is_visible' => false, 'is_built_by_wc' => false, ), 'facebook-for-woocommerce:alt' => array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'description' => __( 'List products and create ads on Facebook and Instagram.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-facebook', 'is_visible' => false, 'is_built_by_wc' => false, ), 'pinterest-for-woocommerce' => array( 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinners searching for ideas and things to buy.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => true, 'min_php_version' => '7.3', ), 'pinterest-for-woocommerce:alt' => array( 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => true, ), 'mailpoet' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ), 'mailchimp-for-woocommerce' => array( 'name' => __( 'Mailchimp', 'woocommerce' ), 'description' => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailchimp-woocommerce', 'is_built_by_wc' => false, ), 'klaviyo' => array( 'name' => __( 'Klaviyo', 'woocommerce' ), 'description' => __( 'Grow and retain customers with intelligent, impactful email and SMS marketing automation and a consolidated view of customer interactions.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/klaviyo.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=klaviyo_settings', 'is_built_by_wc' => false, ), 'woocommerce-payments' => array( 'name' => __( 'WooPayments', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/wcpay.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Accept credit cards and other popular payment methods with %1$sWooPayments%2$s', 'woocommerce' ), '', '' ), 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ES', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CY', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'EE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'LV', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'JP', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AE', 'operation' => '=', ), ), ), DefaultPaymentGateways::get_rules_for_cbd( false ), ), 'is_built_by_wc' => true, 'min_wp_version' => '5.9', ), 'woocommerce-services:shipping' => array( 'name' => __( 'WooCommerce Shipping', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Print shipping labels with %1$sWooCommerce Shipping%2$s', 'woocommerce' ), '', '' ), 'is_visible' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'woocommerce-services' ), ), ), ), array( 'type' => 'or', 'operands' => array( array( array( 'type' => 'option', 'transformers' => array( array( 'use' => 'dot_notation', 'arguments' => array( 'path' => 'product_types', ), ), array( 'use' => 'count', ), ), 'option_name' => 'woocommerce_onboarding_profile', 'value' => 1, 'default' => array(), 'operation' => '!=', ), ), array( array( 'type' => 'option', 'transformers' => array( array( 'use' => 'dot_notation', 'arguments' => array( 'path' => 'product_types.0', ), ), ), 'option_name' => 'woocommerce_onboarding_profile', 'value' => 'downloads', 'default' => '', 'operation' => '!=', ), ), ), ), ), 'is_built_by_wc' => true, ), 'woocommerce-services:tax' => array( 'name' => __( 'WooCommerce Tax', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/woo.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Get automated sales tax with %1$sWooCommerce Tax%2$s', 'woocommerce' ), '', '' ), 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), ), ), array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'woocommerce-services' ), ), ), ), ), 'is_built_by_wc' => true, ), 'jetpack' => array( 'name' => __( 'Jetpack', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/jetpack.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Enhance speed and security with %1$sJetpack%2$s', 'woocommerce' ), '', '' ), 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'jetpack' ), ), ), ), ), 'is_built_by_wc' => false, 'min_wp_version' => '6.0', ), 'mailpoet' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Level up your email marketing with %1$sMailPoet%2$s', 'woocommerce' ), '', '' ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_visible' => array( array( 'type' => 'not', 'operand' => array( array( 'type' => 'plugins_activated', 'plugins' => array( 'mailpoet' ), ), ), ), ), 'is_built_by_wc' => true, ), 'mailpoet:alt' => array( 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ), 'tiktok-for-business' => array( 'name' => __( 'TikTok for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ), 'description' => __( 'Grow your online sales by promoting your products on TikTok to over one billion monthly active users around the world.', 'woocommerce' ), 'manage_url' => 'admin.php?page=tiktok', 'is_visible' => array( array( 'type' => 'or', 'operands' => array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MX', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DK', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FI', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'FR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'DE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'HU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PT', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ES', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'GB', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'CH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NO', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'NZ', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SG', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'MY', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'PH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'ID', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'VN', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'TH', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'KR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'IL', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'AE', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'RU', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'UA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'TR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'SA', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'BR', 'operation' => '=', ), array( 'type' => 'base_location_country', 'value' => 'JP', 'operation' => '=', ), ), ), ), 'is_built_by_wc' => false, ), 'tiktok-for-business:alt' => array( 'name' => __( 'TikTok for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ __( 'Create ad campaigns and reach one billion global users with %1$sTikTok for WooCommerce%2$s', 'woocommerce' ), '', '' ), 'manage_url' => 'admin.php?page=tiktok', 'is_built_by_wc' => false, 'is_visible' => false, ), ); $plugin = $plugins[ $slug ]; $plugin['key'] = $slug; return $plugin; } /** * Decorate plugin data with core profiler fields. * * - Updated description for the core-profiler. * - Adds learn_more_link and label. * - Adds install_priority, which is used to sort the plugins. The value is determined by the plugin size. Lower = smaller. * * @param array $plugins Array of plugins. * * @return array */ public static function with_core_profiler_fields( array $plugins ) { $_plugins = array( 'woocommerce-payments' => array( 'label' => __( 'Get paid with WooPayments', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/woocommerce-payments', 'install_priority' => 5, ), 'woocommerce-services:shipping' => array( 'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/woocommerce-shipping', 'install_priority' => 3, ), 'jetpack' => array( 'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ), 'description' => __( 'Save time on content creation — unlock high-quality blog posts and pages using AI.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/jetpack', 'install_priority' => 8, ), 'pinterest-for-woocommerce' => array( 'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-pinterest.svg', WC_PLUGIN_FILE ), 'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/pinterest-for-woocommerce', 'install_priority' => 2, ), 'mailpoet' => array( 'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-mailpoet.svg', WC_PLUGIN_FILE ), 'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/mailpoet', 'install_priority' => 7, ), 'tiktok-for-business' => array( 'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-tiktok.svg', WC_PLUGIN_FILE ), 'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/tiktok-for-woocommerce', 'install_priority' => 1, ), 'google-listings-and-ads' => array( 'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ), 'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/google-listings-and-ads', 'install_priority' => 6, ), 'woocommerce-services:tax' => array( 'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ), 'learn_more_link' => 'https://woo.com/products/tax', 'install_priority' => 4, ), ); // Copy shipping for the core-profiler and remove is_visible conditions, except for the country restriction. $_plugins['woocommerce-services:shipping']['is_visible'] = [ array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), ]; $remove_plugins_activated_rule = function( $is_visible ) { $is_visible = array_filter( array_map( function( $rule ) { if ( is_object( $rule ) || ! isset( $rule['operand'] ) ) { return $rule; } return array_filter( $rule['operand'], function( $operand ) { return 'plugins_activated' !== $operand['type']; } ); }, $is_visible ) ); return empty( $is_visible ) ? true : $is_visible; }; foreach ( $plugins as &$plugin ) { if ( isset( $_plugins[ $plugin['key'] ] ) ) { $plugin = array_merge( $plugin, $_plugins[ $plugin['key'] ] ); if ( isset( $plugin['is_visible'] ) && is_array( $plugin['is_visible'] ) ) { $plugin['is_visible'] = $remove_plugins_activated_rule( $plugin['is_visible'] ); } } } return $plugins; } } RemoteFreeExtensions/Init.php000064400000004225150732355220012304 0ustar00key, $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.php000064400000003136150732355220015044 0ustar00is_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.php000064400000001371150732355220020646 0ustar00 'key', ) ); } return self::$instance; } } ProductForm/ComponentTrait.php000064400000001315150732355220012473 0ustar00id; } /** * Return plugin id. * * @return string */ public function get_plugin_id() { return $this->plugin_id; } } ProductForm/FormFactory.php000064400000016476150732355220011776 0ustar00 (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.php000064400000002234150732355220011132 0ustar00 (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.php000064400000000304150732355220011640 0ustar00 (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.php000064400000002322150732355220010232 0ustar00 (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.php000064400000005547150732355220011502 0ustar00id = $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.php000064400000021552150732355220006532 0ustar00possibly_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.php000064400000001400150732355220006551 0ustar00query( "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.php000064400000046365150732355220006505 0ustar00 array(), 'style' => array(), ); /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. * Hooks added here should be removed in `wc_admin_initialize` via the feature plugin. */ public function __construct() { Features::get_instance(); WCAdminSharedSettings::get_instance(); Translations::get_instance(); WCAdminUser::get_instance(); Settings::get_instance(); SiteHealth::get_instance(); SystemStatusReport::get_instance(); wc_get_container()->get( Reviews::class ); wc_get_container()->get( ReviewsCommentsOverrides::class ); wc_get_container()->get( BlockTemplatesController::class ); add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) ); add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) ); add_action( 'in_admin_header', array( __CLASS__, 'embed_page_header' ) ); add_action( 'admin_head', array( __CLASS__, 'remove_notices' ) ); add_action( 'admin_head', array( __CLASS__, 'smart_app_banner' ) ); add_action( 'admin_notices', array( __CLASS__, 'inject_before_notices' ), -9999 ); add_action( 'admin_notices', array( __CLASS__, 'inject_after_notices' ), PHP_INT_MAX ); // Added this hook to delete the field woocommerce_onboarding_homepage_post_id when deleting the homepage. add_action( 'trashed_post', array( __CLASS__, 'delete_homepage' ) ); /* * Remove the emoji script as it always defaults to replacing emojis with Twemoji images. * Gutenberg has also disabled emojis. More on that here -> https://github.com/WordPress/gutenberg/pull/6151 */ remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) ); } /** * Returns breadcrumbs for the current page. */ private static function get_embed_breadcrumbs() { return wc_admin_get_breadcrumbs(); } /** * Outputs breadcrumbs via PHP for the initial load of an embedded page. * * @param array $section Section to create breadcrumb from. */ private static function output_heading( $section ) { echo esc_html( $section ); } /** * Set up a div for the header embed to render into. * The initial contents here are meant as a place loader for when the PHP page initialy loads. */ public static function embed_page_header() { if ( ! PageController::is_admin_page() && ! PageController::is_embed_page() ) { return; } if ( ! PageController::is_embed_page() ) { return; } $sections = self::get_embed_breadcrumbs(); $sections = is_array( $sections ) ? $sections : array( $sections ); ?>

). * * @param bool $is_loading If WooCommerce Admin is loading a fullscreen view. */ $is_loading = apply_filters( 'woocommerce_admin_is_loading', false ); if ( PageController::is_admin_page() && $is_loading ) { $classes[] = 'woocommerce-admin-is-loading'; } $admin_body_class = implode( ' ', array_unique( $classes ) ); return " $admin_body_class "; } /** * Adds an iOS "Smart App Banner" for display on iOS Safari. * See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html */ public static function smart_app_banner() { if ( PageController::is_admin_or_embed_page() ) { echo " "; } } /** * Removes notices that should not be displayed on WC Admin pages. */ public static function remove_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // Hello Dolly. if ( function_exists( 'hello_dolly' ) ) { remove_action( 'admin_notices', 'hello_dolly' ); } } /** * Runs before admin notices action and hides them. */ public static function inject_before_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // The JITMs won't be shown in the Onboarding Wizard. $is_onboarding = isset( $_GET['path'] ) && '/setup-wizard' === wc_clean( wp_unslash( $_GET['path'] ) ); // phpcs:ignore WordPress.Security.NonceVerification $maybe_hide_jitm = $is_onboarding ? '-hide' : ''; echo '
'; // Wrap the notices in a hidden div to prevent flickering before // they are moved elsewhere in the page by WordPress Core. echo '
'; if ( PageController::is_admin_page() ) { // Capture all notices and hide them. WordPress Core looks for // `.wp-header-end` and appends notices after it if found. // https://github.com/WordPress/WordPress/blob/f6a37e7d39e2534d05b9e542045174498edfe536/wp-admin/js/common.js#L737 . echo '
'; } } /** * Runs after admin notices and closes div. */ public static function inject_after_notices() { if ( ! PageController::is_admin_or_embed_page() ) { return; } // Close the hidden div used to prevent notices from flickering before // they are inserted elsewhere in the page. echo '
'; } /** * Edits Admin title based on section of wc-admin. * * @param string $admin_title Modifies admin title. * @todo Can we do some URL rewriting so we can figure out which page they are on server side? */ public static function update_admin_title( $admin_title ) { if ( ! did_action( 'current_screen' ) || ! PageController::is_admin_page() ) { return $admin_title; } $sections = self::get_embed_breadcrumbs(); $pieces = array(); foreach ( $sections as $section ) { $pieces[] = is_array( $section ) ? $section[1] : $section; } $pieces = array_reverse( $pieces ); $title = implode( ' ‹ ', $pieces ); /* translators: %1$s: updated title, %2$s: blog info name */ return sprintf( __( '%1$s ‹ %2$s', 'woocommerce' ), $title, get_bloginfo( 'name' ) ); } /** * Set up a div for the app to render into. */ public static function page_wrapper() { ?>
isset( $settings['siteLocale'] ) ? $settings['siteLocale'] : get_locale(), 'userLocale' => isset( $settings['l10n']['userLocale'] ) ? $settings['l10n']['userLocale'] : get_user_locale(), 'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] ) ? $settings['l10n']['weekdaysShort'] : array_values( $wp_locale->weekday_abbrev ), ]; } $preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() ); $preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection'; if ( ! empty( $preload_data_endpoints ) ) { $preload_data = array_reduce( array_values( $preload_data_endpoints ), 'rest_preload_api_request' ); } $preload_options = apply_filters( 'woocommerce_admin_preload_options', array() ); if ( ! empty( $preload_options ) ) { foreach ( $preload_options as $option ) { $settings['preloadOptions'][ $option ] = get_option( $option ); } } $preload_settings = apply_filters( 'woocommerce_admin_preload_settings', array() ); if ( ! empty( $preload_settings ) ) { $setting_options = new \WC_REST_Setting_Options_V2_Controller(); foreach ( $preload_settings as $group ) { $group_settings = $setting_options->get_group_settings( $group ); $preload_settings = []; foreach ( $group_settings as $option ) { if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) { $preload_settings[ $option['id'] ] = $option['value']; } } $settings['preloadSettings'][ $group ] = $preload_settings; } } $user_controller = new \WP_REST_Users_Controller(); $request = new \WP_REST_Request(); $request->set_query_params( array( 'context' => 'edit' ) ); $user_response = $user_controller->get_current_item( $request ); $current_user_data = is_wp_error( $user_response ) ? (object) array() : $user_response->get_data(); $settings['currentUserData'] = $current_user_data; $settings['reviewsEnabled'] = get_option( 'woocommerce_enable_reviews' ); $settings['manageStock'] = get_option( 'woocommerce_manage_stock' ); $settings['commentModeration'] = get_option( 'comment_moderation' ); $settings['notifyLowStockAmount'] = get_option( 'woocommerce_notify_low_stock_amount' ); // @todo On merge, once plugin images are added to core WooCommerce, `wcAdminAssetUrl` can be retired, // and `wcAssetUrl` can be used in its place throughout the codebase. $settings['wcAdminAssetUrl'] = WC_ADMIN_IMAGES_FOLDER_URL; $settings['wcVersion'] = WC_VERSION; $settings['siteUrl'] = site_url(); $settings['shopUrl'] = get_permalink( wc_get_page_id( 'shop' ) ); $settings['homeUrl'] = home_url(); $settings['dateFormat'] = get_option( 'date_format' ); $settings['timeZone'] = wc_timezone_string(); $settings['plugins'] = array( 'installedPlugins' => PluginsHelper::get_installed_plugin_slugs(), 'activePlugins' => Plugins::get_active_plugins(), ); // Plugins that depend on changing the translation work on the server but not the client - // WooCommerce Branding is an example of this - so pass through the translation of // 'WooCommerce' to wcSettings. $settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce' ); // We may have synced orders with a now-unregistered status. // E.g An extension that added statuses is now inactive or removed. $settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses(); // The separator used for attributes found in Variation titles. $settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() ); if ( ! empty( $preload_data_endpoints ) ) { $settings['dataEndpoints'] = isset( $settings['dataEndpoints'] ) ? $settings['dataEndpoints'] : []; foreach ( $preload_data_endpoints as $key => $endpoint ) { // Handle error case: rest_do_request() doesn't guarantee success. if ( empty( $preload_data[ $endpoint ] ) ) { $settings['dataEndpoints'][ $key ] = array(); } else { $settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body']; } } } $settings = self::get_custom_settings( $settings ); if ( PageController::is_embed_page() ) { $settings['embedBreadcrumbs'] = self::get_embed_breadcrumbs(); } $settings['allowMarketplaceSuggestions'] = WC_Marketplace_Suggestions::allow_suggestions(); $settings['connectNonce'] = wp_create_nonce( 'connect' ); $settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' ); return $settings; } /** * Format order statuses by removing a leading 'wc-' if present. * * @param array $statuses Order statuses. * @return array formatted statuses. */ public static function get_order_statuses( $statuses ) { $formatted_statuses = array(); foreach ( $statuses as $key => $value ) { $formatted_key = preg_replace( '/^wc-/', '', $key ); $formatted_statuses[ $formatted_key ] = $value; } return $formatted_statuses; } /** * Get all order statuses present in analytics tables that aren't registered. * * @return array Unregistered order statuses. */ public static function get_unregistered_order_statuses() { $registered_statuses = wc_get_order_statuses(); $all_synced_statuses = OrdersDataStore::get_all_statuses(); $unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) ); $formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) ); $formatted_statuses = array_keys( $formatted_status_keys ); return array_combine( $formatted_statuses, $formatted_statuses ); } /** * Register the admin settings for use in the WC REST API * * @param array $groups Array of setting groups. * @return array */ public static function add_settings_group( $groups ) { $groups[] = array( 'id' => 'wc_admin', 'label' => __( 'WooCommerce Admin', 'woocommerce' ), 'description' => __( 'Settings for WooCommerce admin reporting.', 'woocommerce' ), ); return $groups; } /** * Add WC Admin specific settings * * @param array $settings Array of settings in wc admin group. * @return array */ public static function add_settings( $settings ) { $unregistered_statuses = self::get_unregistered_order_statuses(); $registered_statuses = self::get_order_statuses( wc_get_order_statuses() ); $all_statuses = array_merge( $unregistered_statuses, $registered_statuses ); $settings[] = array( 'id' => 'woocommerce_excluded_report_order_statuses', 'option_key' => 'woocommerce_excluded_report_order_statuses', 'label' => __( 'Excluded report order statuses', 'woocommerce' ), 'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce' ), 'default' => array( 'pending', 'cancelled', 'failed' ), 'type' => 'multiselect', 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_actionable_order_statuses', 'option_key' => 'woocommerce_actionable_order_statuses', 'label' => __( 'Actionable order statuses', 'woocommerce' ), 'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce' ), 'default' => array( 'processing', 'on-hold' ), 'type' => 'multiselect', 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_default_date_range', 'option_key' => 'woocommerce_default_date_range', 'label' => __( 'Default Date Range', 'woocommerce' ), 'description' => __( 'Default Date Range', 'woocommerce' ), 'default' => 'period=month&compare=previous_year', 'type' => 'text', ); return $settings; } /** * Gets custom settings used for WC Admin. * * @param array $settings Array of settings to merge into. * @return array */ public static function get_custom_settings( $settings ) { $wc_rest_settings_options_controller = new \WC_REST_Setting_Options_Controller(); $wc_admin_group_settings = $wc_rest_settings_options_controller->get_group_settings( 'wc_admin' ); $settings['wcAdminSettings'] = array(); foreach ( $wc_admin_group_settings as $setting ) { if ( ! empty( $setting['id'] ) ) { $settings['wcAdminSettings'][ $setting['id'] ] = $setting['value']; } } return $settings; } /** * Return an object defining the currecy options for the site's current currency * * @return array Settings for the current currency { * Array of settings. * * @type string $code Currency code. * @type string $precision Number of decimals. * @type string $symbol Symbol for currency. * } */ public static function get_currency_settings() { $code = get_woocommerce_currency(); return apply_filters( 'wc_currency_settings', array( 'code' => $code, 'precision' => wc_get_price_decimals(), 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ), 'symbolPosition' => get_option( 'woocommerce_currency_pos' ), 'decimalSeparator' => wc_get_price_decimal_separator(), 'thousandSeparator' => wc_get_price_thousand_separator(), 'priceFormat' => html_entity_decode( get_woocommerce_price_format() ), ) ); } /** * Delete woocommerce_onboarding_homepage_post_id field when the homepage is deleted * * @param int $post_id The deleted post id. */ public static function delete_homepage( $post_id ) { if ( 'page' !== get_post_type( $post_id ) ) { return; } $homepage_id = intval( get_option( 'woocommerce_onboarding_homepage_post_id', false ) ); if ( $homepage_id === $post_id ) { delete_option( 'woocommerce_onboarding_homepage_post_id' ); } } /** * Adds the appearance_theme_view Tracks event. */ public static function add_appearance_theme_view_tracks_event() { wc_admin_record_tracks_event( 'appearance_theme_view', array() ); } } ShippingLabelBanner.php000064400000010226150732355220011131 0ustar00shipping_label_banner_display_rules ) { $jetpack_version = null; $jetpack_connected = null; $wcs_version = null; $wcs_tos_accepted = null; if ( defined( 'JETPACK__VERSION' ) ) { $jetpack_version = JETPACK__VERSION; } if ( class_exists( Jetpack_Connection_Manager::class ) ) { $jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner(); } if ( class_exists( '\WC_Connect_Loader' ) ) { $wcs_version = \WC_Connect_Loader::get_wcs_version(); } if ( class_exists( '\WC_Connect_Options' ) ) { $wcs_tos_accepted = \WC_Connect_Options::get_option( 'tos_accepted' ); } $incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) || class_exists( '\WC_Shipping_UPS_Init' ) || class_exists( '\WC_Integration_ShippingEasy' ) || class_exists( '\WC_ShipStation_Integration' ); $this->shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( $jetpack_version, $jetpack_connected, $wcs_version, $wcs_tos_accepted, $incompatible_plugins ); } return $this->shipping_label_banner_display_rules->should_display_banner(); } /** * Add metabox to order page. * * @param string $post_type current post type. * @param \WP_Post $post Current post object. */ public function add_meta_boxes( $post_type, $post ) { if ( 'shop_order' !== $post_type ) { return; } $order = wc_get_order( $post ); if ( $this->should_show_meta_box() ) { add_meta_box( 'woocommerce-admin-print-label', __( 'Shipping Label', 'woocommerce' ), array( $this, 'meta_box' ), null, 'normal', 'high', array( 'context' => 'shipping_label', 'order' => $post->ID, 'items' => $this->count_shippable_items( $order ), ) ); add_action( 'admin_enqueue_scripts', array( $this, 'add_print_shipping_label_script' ) ); } } /** * Count shippable items * * @param \WC_Order $order Current order. * @return int */ private function count_shippable_items( \WC_Order $order ) { $count = 0; foreach ( $order->get_items() as $item ) { if ( $item instanceof \WC_Order_Item_Product ) { $product = $item->get_product(); if ( $product && $product->needs_shipping() ) { $count += $item->get_quantity(); } } } return $count; } /** * Adds JS to order page to render shipping banner. * * @param string $hook current page hook. */ public function add_print_shipping_label_script( $hook ) { $rtl = is_rtl() ? '.rtl' : ''; wp_enqueue_style( 'print-shipping-label-banner-style', WCAdminAssets::get_url( "print-shipping-label-banner/style{$rtl}", 'css' ), array( 'wp-components' ), WCAdminAssets::get_file_version( 'css' ) ); WCAdminAssets::register_script( 'wp-admin-scripts', 'print-shipping-label-banner', true ); $payload = array( 'nonce' => wp_create_nonce( 'wp_rest' ), 'baseURL' => get_rest_url(), 'wcs_server_connection' => true, ); wp_localize_script( 'print-shipping-label-banner', 'wcConnectData', $payload ); } /** * Render placeholder metabox. * * @param \WP_Post $post current post. * @param array $args empty args. */ public function meta_box( $post, $args ) { ?>
id, wc_get_screen_ids(), true ) ) { return; } // Remove the old help tab if it exists. $help_tabs = $screen->get_help_tabs(); foreach ( $help_tabs as $help_tab ) { if ( 'woocommerce_onboard_tab' !== $help_tab['id'] ) { continue; } $screen->remove_help_tab( 'woocommerce_onboard_tab' ); } // Add the new help tab. $help_tab = array( 'title' => __( 'Setup wizard', 'woocommerce' ), 'id' => 'woocommerce_onboard_tab', ); $setup_list = TaskLists::get_list( 'setup' ); $extended_list = TaskLists::get_list( 'extended' ); if ( $setup_list ) { $help_tab['content'] = '

' . __( 'WooCommerce Onboarding', 'woocommerce' ) . '

'; $help_tab['content'] .= '

' . __( 'Profile Setup Wizard', 'woocommerce' ) . '

'; $help_tab['content'] .= '

' . __( 'If you need to access the setup wizard again, please click on the button below.', 'woocommerce' ) . '

' . '

' . __( 'Setup wizard', 'woocommerce' ) . '

'; $help_tab['content'] .= '

' . __( 'Task List', 'woocommerce' ) . '

'; $help_tab['content'] .= '

' . __( 'If you need to enable or disable the task lists, please click on the button below.', 'woocommerce' ) . '

' . ( $setup_list->is_hidden() ? '

' . __( 'Enable', 'woocommerce' ) . '

' : '

' . __( 'Disable', 'woocommerce' ) . '

' ); } if ( $extended_list ) { $help_tab['content'] .= '

' . __( 'Extended task List', 'woocommerce' ) . '

'; $help_tab['content'] .= '

' . __( 'If you need to enable or disable the extended task lists, please click on the button below.', 'woocommerce' ) . '

' . ( $extended_list->is_hidden() ? '

' . __( 'Enable', 'woocommerce' ) . '

' : '

' . __( 'Disable', 'woocommerce' ) . '

' ); } $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.php000064400000003564150732355220012756 0ustar00 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.php000064400000002301150732355220013245 0ustar00is_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.php000064400000007701150732355220012267 0ustar00 '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.php000064400000015656150732355220012610 0ustar00 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.php000064400000001073150732355220011426 0ustar00init(); OnboardingIndustries::init(); OnboardingJetpack::instance()->init(); OnboardingMailchimp::instance()->init(); OnboardingProfile::init(); OnboardingSetupWizard::instance()->init(); OnboardingSync::instance()->init(); OnboardingThemes::init(); } } Onboarding/OnboardingProducts.php000064400000012534150732355220013156 0ustar00 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.php000064400000003464150732355220012736 0ustar00is_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.php000064400000015012150732355220010032 0ustar00define_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.php000064400000005405150732355220013404 0ustar00get( 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.php000064400000036731150732355220013152 0ustar00get_meta_data(); $metadata_to_list = array(); foreach ( $metadata as $meta ) { $data = $meta->get_data(); if ( is_protected_meta( $data['key'], 'order' ) ) { continue; } $metadata_to_list[] = array( 'meta_id' => $data['id'], 'meta_key' => $data['key'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- False positive, not a meta query. 'meta_value' => maybe_serialize( $data['value'] ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- False positive, not a meta query. ); } return $metadata_to_list; } /** * Renders the meta box to manage custom meta. * * @param \WP_Post|\WC_Order $order_or_post Post or order object that we are rendering for. */ public function output( $order_or_post ) { if ( is_a( $order_or_post, \WP_Post::class ) ) { $order = wc_get_order( $order_or_post ); } else { $order = $order_or_post; } $this->render_custom_meta_form( $this->get_formatted_order_meta_data( $order ), $order ); } /** * Helper method to render layout and actual HTML * * @param array $metadata_to_list List of metadata to render. * @param \WC_Order $order Order object. */ private function render_custom_meta_form( array $metadata_to_list, \WC_Order $order ) { ?>
render_meta_form( $order ); ?>

', '' ); ?>

order_meta_keys_autofill( null, $order ); /** * Filters values for the meta key dropdown in the Custom Fields meta box. * * Compatibility filter for `postmeta_form_keys` filter. * * @since 6.9.0 * * @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null. * @param \WC_Order $order The current post object. */ $keys = apply_filters( 'postmeta_form_keys', $keys, $order ); ?>

'newmeta-submit', 'data-wp-lists' => 'add:the-list:newmeta', ) ); ?>
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"; $r .= "\n\t\t"; $r .= "\n\t\t
"; $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 .= '
'; $r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false ); $r .= ''; $r .= "\n\t\t\n\t"; 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.php000064400000002433150732355220013573 0ustar00display_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.php000064400000010430150732355220014012 0ustar00orders_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.php000064400000003424150732355220013711 0ustar00set_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.php000064400000136451150732355220010414 0ustar00 '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 ''; $this->single_row_columns( $order ); echo ''; } /** * 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 = ''; $search_label .= sprintf( /* translators: %s: Search query. */ __( 'Search results for: %s', 'woocommerce' ), '' . esc_html( $this->order_query_args['s'] ) . '' ); $search_label .= ''; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wp_kses_post( "

{$title}

{$add_new} {$search_label}
" ); if ( $this->should_render_blank_state() ) { $this->render_blank_state(); return; } $this->views(); echo '
'; $this->print_hidden_form_fields(); $this->search_box( esc_html__( 'Search orders', 'woocommerce' ), 'orders-search-input' ); parent::display(); echo '
'; } /** * Renders advice in the event that no orders exist yet. * * @return void */ public function render_blank_state(): void { ?>

order_query_args['status'] ?? false; if ( array( 'trash' ) === $selected_status ) { $actions = array( 'untrash' => __( 'Restore', 'woocommerce' ), 'delete' => __( 'Delete permanently', 'woocommerce' ), ); } else { $actions = array( 'mark_processing' => __( 'Change status to processing', 'woocommerce' ), 'mark_on-hold' => __( 'Change status to on-hold', 'woocommerce' ), 'mark_completed' => __( 'Change status to completed', 'woocommerce' ), 'mark_cancelled' => __( 'Change status to cancelled', 'woocommerce' ), 'trash' => __( 'Move to Trash', 'woocommerce' ), ); } if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) { $actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' ); } return $actions; } /** * Gets a list of CSS classes for the WP_List_Table table tag. * * @since 7.8.0 * * @return string[] Array of CSS classes for the table tag. */ protected function get_table_classes() { /** * Filters the list of CSS class names for the orders list table. * * @since 7.8.0 * * @param string[] $classes An array of CSS class names. * @param string $order_type The order type. */ $css_classes = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_css_classes', array_merge( parent::get_table_classes(), array( 'wc-orders-list-table', 'wc-orders-list-table-' . $this->order_type, ) ), $this->order_type ); return array_unique( array_map( 'trim', $css_classes ) ); } /** * Prepares the list of items for displaying. */ public function prepare_items() { $limit = $this->get_items_per_page( 'edit_' . $this->order_type . '_per_page' ); $this->order_query_args = array( 'limit' => $limit, 'page' => $this->get_pagenum(), 'paginate' => true, 'type' => $this->order_type, ); foreach ( array( 'status', 's', 'm', '_customer_user' ) as $query_var ) { $this->request[ $query_var ] = sanitize_text_field( wp_unslash( $_REQUEST[ $query_var ] ?? '' ) ); } /** * Allows 3rd parties to filter the initial request vars before defaults and other logic is applied. * * @param array $request Request to be passed to `wc_get_orders()`. * * @since 7.3.0 */ $this->request = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_request', $this->request ); $this->set_status_args(); $this->set_order_args(); $this->set_date_args(); $this->set_customer_args(); $this->set_search_args(); /** * Provides an opportunity to modify the query arguments used in the (Custom Order Table-powered) order list * table. * * @since 6.9.0 * * @param array $query_args Arguments to be passed to `wc_get_orders()`. */ $order_query_args = (array) apply_filters( 'woocommerce_order_list_table_prepare_items_query_args', $this->order_query_args ); /** * Same as `woocommerce_order_list_table_prepare_items_query_args` but for a specific order type. * * @param array $query_args Arguments to be passed to `wc_get_orders()`. * * @since 7.3.0 */ $order_query_args = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_prepare_items_query_args', $order_query_args ); // We must ensure the 'paginate' argument is set. $order_query_args['paginate'] = true; $orders = wc_get_orders( $order_query_args ); $this->items = $orders->orders; $max_num_pages = $orders->max_num_pages; // Check in case the user has attempted to page beyond the available range of orders. if ( 0 === $max_num_pages && $this->order_query_args['page'] > 1 ) { $count_query_args = $order_query_args; $count_query_args['page'] = 1; $count_query_args['limit'] = 1; $order_count = wc_get_orders( $count_query_args ); $max_num_pages = (int) ceil( $order_count->total / $order_query_args['limit'] ); } $this->set_pagination_args( array( 'total_items' => $orders->total ?? 0, 'per_page' => $limit, 'total_pages' => $max_num_pages, ) ); // Are we inside the trash? $this->is_trash = 'trash' === $this->request['status']; } /** * Updates the WC Order Query arguments as needed to support orderable columns. */ private function set_order_args() { $sortable = $this->get_sortable_columns(); $field = sanitize_text_field( wp_unslash( $_GET['orderby'] ?? '' ) ); $direction = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ?? '' ) ) ); if ( ! in_array( $field, $sortable, true ) ) { $this->order_query_args['orderby'] = 'date'; $this->order_query_args['order'] = 'DESC'; return; } $this->order_query_args['orderby'] = $field; $this->order_query_args['order'] = in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC'; } /** * Implements date (month-based) filtering. */ private function set_date_args() { $year_month = sanitize_text_field( wp_unslash( $_GET['m'] ?? '' ) ); if ( empty( $year_month ) || ! preg_match( '/^[0-9]{6}$/', $year_month ) ) { return; } $year = (int) substr( $year_month, 0, 4 ); $month = (int) substr( $year_month, 4, 2 ); if ( $month < 0 || $month > 12 ) { return; } $last_day_of_month = date_create( "$year-$month" )->format( 'Y-m-t' ); $this->order_query_args['date_created'] = "$year-$month-01..." . $last_day_of_month; $this->has_filter = true; } /** * Implements filtering of orders by customer. */ private function set_customer_args() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $customer = (int) wp_unslash( $_GET['_customer_user'] ?? '' ); if ( $customer < 1 ) { return; } $this->order_query_args['customer'] = $customer; $this->has_filter = true; } /** * Implements filtering of orders by status. */ private function set_status_args() { $status = array_filter( array_map( 'trim', (array) $this->request['status'] ) ); if ( empty( $status ) || in_array( 'all', $status, true ) ) { /** * Allows 3rd parties to set the default list of statuses for a given order type. * * @param string[] $statuses Statuses. * * @since 7.3.0 */ $status = apply_filters( 'woocommerce_' . $this->order_type . '_list_table_default_statuses', array_intersect( array_keys( wc_get_order_statuses() ), get_post_stati( array( 'show_in_admin_all_list' => true ), 'names' ) ) ); } else { $this->has_filter = true; } $this->order_query_args['status'] = $status; } /** * Implements order search. */ private function set_search_args(): void { $search_term = trim( sanitize_text_field( $this->request['s'] ) ); if ( ! empty( $search_term ) ) { $this->order_query_args['s'] = $search_term; $this->has_filter = true; } } /** * Get the list of views for this table (all orders, completed orders, etc, each with a count of the number of * corresponding orders). * * @return array */ public function get_views() { $view_counts = array(); $view_links = array(); $statuses = $this->get_visible_statuses(); $current = ! empty( $this->request['status'] ) ? sanitize_text_field( $this->request['status'] ) : 'all'; $all_count = 0; foreach ( array_keys( $statuses ) as $slug ) { $total_in_status = $this->count_orders_by_status( $slug ); if ( $total_in_status > 0 ) { $view_counts[ $slug ] = $total_in_status; } if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list && 'auto-draft' !== $slug ) { $all_count += $total_in_status; } } $view_links['all'] = $this->get_view_link( 'all', __( 'All', 'woocommerce' ), $all_count, '' === $current || 'all' === $current ); foreach ( $view_counts as $slug => $count ) { $view_links[ $slug ] = $this->get_view_link( $slug, $statuses[ $slug ], $count, $slug === $current ); } return $view_links; } /** * Count orders by status. * * @param string|string[] $status The order status we are interested in. * * @return int */ private function count_orders_by_status( $status ): int { global $wpdb; // Compute all counts and cache if necessary. if ( is_null( $this->status_count_cache ) ) { $orders_table = OrdersTableDataStore::get_orders_table_name(); $res = $wpdb->get_results( $wpdb->prepare( "SELECT status, COUNT(*) AS cnt FROM {$orders_table} WHERE type = %s GROUP BY status", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $this->order_type ), ARRAY_A ); $this->status_count_cache = $res ? array_combine( array_column( $res, 'status' ), array_map( 'absint', array_column( $res, 'cnt' ) ) ) : array(); } $status = (array) $status; $count = array_sum( array_intersect_key( $this->status_count_cache, array_flip( $status ) ) ); /** * Allows 3rd parties to modify the count of orders by status. * * @param int $count Number of orders for the given status. * @param string[] $status List of order statuses in the count. * @since 7.3.0 */ return apply_filters( 'woocommerce_' . $this->order_type . '_list_table_order_count', $count, $status ); } /** * Checks whether the blank state should be rendered or not. This depends on whether there are others with a visible * status. * * @return boolean TRUE when the blank state should be rendered, FALSE otherwise. */ private function should_render_blank_state(): bool { return ( ! $this->has_filter ) && 0 === $this->count_orders_by_status( array_keys( $this->get_visible_statuses() ) ); } /** * Returns a list of slug and labels for order statuses that should be visible in the status list. * * @return array slug => label array of order statuses. */ private function get_visible_statuses(): array { return array_intersect_key( array_merge( wc_get_order_statuses(), array( 'trash' => ( get_post_status_object( 'trash' ) )->label, 'draft' => ( get_post_status_object( 'draft' ) )->label, 'auto-draft' => ( get_post_status_object( 'auto-draft' ) )->label, ) ), array_flip( get_post_stati( array( 'show_in_admin_status_list' => true ) ) ) ); } /** * Form a link to use in the list of table views. * * @param string $slug Slug used to identify the view (usually the order status slug). * @param string $name Human-readable name of the view (usually the order status label). * @param int $count Number of items in this view. * @param bool $current If this is the current view. * * @return string */ private function get_view_link( string $slug, string $name, int $count, bool $current ): string { $base_url = get_admin_url( null, 'admin.php?page=wc-orders' . ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type ) ); $url = esc_url( add_query_arg( 'status', $slug, $base_url ) ); $name = esc_html( $name ); $count = absint( $count ); $class = $current ? 'class="current"' : ''; return "$name ($count)"; } /** * Extra controls to be displayed between bulk actions and pagination. * * @param string $which Either 'top' or 'bottom'. */ protected function extra_tablenav( $which ) { echo '
'; if ( 'top' === $which ) { ob_start(); $this->months_filter(); /** * Fires before the "Filter" button on the list table for orders and other order types. * * @since 7.3.0 * * @param string $order_type The order type. * @param string $which The location of the extra table nav: 'top' or 'bottom'. */ do_action( 'woocommerce_order_list_table_restrict_manage_orders', $this->order_type, $which ); $output = ob_get_clean(); if ( ! empty( $output ) ) { echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'order-query-submit' ) ); } } if ( $this->is_trash && $this->has_items() && current_user_can( 'edit_others_shop_orders' ) ) { submit_button( __( 'Empty Trash', 'woocommerce' ), 'apply', 'delete_all', false ); } /** * Fires immediately following the closing "actions" div in the tablenav for the order * list table. * * @since 7.3.0 * * @param string $order_type The order type. * @param string $which The location of the extra table nav: 'top' or 'bottom'. */ do_action( 'woocommerce_order_list_table_extra_tablenav', $this->order_type, $which ); echo '
'; } /** * Render the months filter dropdown. * * @return void */ private function months_filter() { // XXX: [review] we may prefer to move this logic outside of the ListTable class. global $wp_locale; global $wpdb; $orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() ); $utc_offset = wc_timezone_offset(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $order_dates = $wpdb->get_results( " SELECT DISTINCT YEAR( t.date_created_local ) AS year, MONTH( t.date_created_local ) AS month FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE status != 'trash' ) t ORDER BY year DESC, month DESC " ); $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0; echo ''; } /** * Render the customer filter dropdown. * * @return void */ public function customers_filter() { $user_string = ''; $user_id = ''; // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['_customer_user'] ) ) { $user_id = absint( $_GET['_customer_user'] ); $user = get_user_by( 'id', $user_id ); $user_string = sprintf( /* translators: 1: user display name 2: user ID 3: user email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), $user->display_name, absint( $user->ID ), $user->user_email ); } // Note: use of htmlspecialchars (below) is to prevent XSS when rendered by selectWoo. ?> order_type . '_list_table_columns', array( 'cb' => '', 'order_number' => esc_html__( 'Order', 'woocommerce' ), 'order_date' => esc_html__( 'Date', 'woocommerce' ), 'order_status' => esc_html__( 'Status', 'woocommerce' ), 'billing_address' => esc_html__( 'Billing', 'woocommerce' ), 'shipping_address' => esc_html__( 'Ship to', 'woocommerce' ), 'order_total' => esc_html__( 'Total', 'woocommerce' ), 'wc_actions' => esc_html__( 'Actions', 'woocommerce' ), ) ); } /** * Defines the default sortable columns. * * @return string[] */ public function get_sortable_columns() { /** * Filters the list of sortable columns. * * @param array $sortable_columns List of sortable columns. * * @since 7.3.0 */ return apply_filters( 'woocommerce_' . $this->order_type . '_list_table_sortable_columns', array( 'order_number' => 'ID', 'order_date' => 'date', 'order_total' => 'order_total', ) ); } /** * Specify the columns we wish to hide by default. * * @param array $hidden Columns set to be hidden. * @param WP_Screen $screen Screen object. * * @return array */ public function default_hidden_columns( array $hidden, WP_Screen $screen ) { if ( isset( $screen->id ) && wc_get_page_screen_id( 'shop-order' ) === $screen->id ) { $hidden = array_merge( $hidden, array( 'billing_address', 'shipping_address', 'wc_actions', ) ); } return $hidden; } /** * Checklist column, used for selecting items for processing by a bulk action. * * @param WC_Order $item The order object for the current row. * * @return string */ public function column_cb( $item ) { ob_start(); ?>
get_id() ) ); ?>
get_billing_first_name() || $order->get_billing_last_name() ) { /* translators: 1: first name 2: last name */ $buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) ); } elseif ( $order->get_billing_company() ) { $buyer = trim( $order->get_billing_company() ); } elseif ( $order->get_customer_id() ) { $user = get_user_by( 'id', $order->get_customer_id() ); $buyer = ucwords( $user->display_name ); } /** * Filter buyer name in list table orders. * * @since 3.7.0 * * @param string $buyer Buyer name. * @param WC_Order $order Order data. */ $buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $order ); if ( $order->get_status() === 'trash' ) { echo '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; } else { echo '' . esc_html( __( 'Preview', 'woocommerce' ) ) . ''; echo '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; } } /** * Get the edit link for an order. * * @param WC_Order $order Order object. * * @return string Edit link for the order. */ private function get_order_edit_link( WC_Order $order ) : string { return $this->page_controller->get_edit_url( $order->get_id() ); } /** * Renders the order date. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_order_date_column( WC_Order $order ): void { $order_timestamp = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : ''; if ( ! $order_timestamp ) { echo '–'; return; } // Check if the order was created within the last 24 hours, and not in the future. if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) { $show_date = sprintf( /* translators: %s: human-readable time difference */ _x( '%s ago', '%s = human-readable time difference', 'woocommerce' ), human_time_diff( $order->get_date_created()->getTimestamp(), time() ) ); } else { $show_date = $order->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } printf( '', esc_attr( $order->get_date_created()->date( 'c' ) ), esc_html( $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), esc_html( $show_date ) ); } /** * Renders the order status. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_order_status_column( WC_Order $order ): void { $tooltip = ''; remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $comment_count = get_comment_count( $order->get_id() ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $approved_comments_count = absint( $comment_count['approved'] ); if ( $approved_comments_count ) { $latest_notes = wc_get_order_notes( array( 'order_id' => $order->get_id(), 'limit' => 1, 'orderby' => 'date_created_gmt', ) ); $latest_note = current( $latest_notes ); if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) { $tooltip = wc_sanitize_tooltip( $latest_note->content ); } elseif ( isset( $latest_note->content ) ) { /* translators: %d: notes count */ $tooltip = wc_sanitize_tooltip( $latest_note->content . '
' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '' ); } else { /* translators: %d: notes count */ $tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) ); } } // Gracefully handle legacy statuses. if ( in_array( $order->get_status(), array( 'trash', 'draft', 'auto-draft' ), true ) ) { $status_name = ( get_post_status_object( $order->get_status() ) )->label; } else { $status_name = wc_get_order_status_name( $order->get_status() ); } if ( $tooltip ) { printf( '%s', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), wp_kses_post( $tooltip ), esc_html( $status_name ) ); } else { printf( '%s', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), esc_html( $status_name ) ); } } /** * Renders order billing information. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_billing_address_column( WC_Order $order ): void { $address = $order->get_formatted_billing_address(); if ( $address ) { echo esc_html( preg_replace( '##i', ', ', $address ) ); if ( $order->get_payment_method() ) { /* translators: %s: payment method */ echo '' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_payment_method_title() ) ) . ''; } } else { echo '–'; } } /** * Renders order shipping information. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_shipping_address_column( WC_Order $order ): void { $address = $order->get_formatted_shipping_address(); if ( $address ) { echo '' . esc_html( preg_replace( '##i', ', ', $address ) ) . ''; if ( $order->get_shipping_method() ) { /* translators: %s: shipping method */ echo '' . sprintf( esc_html__( 'via %s', 'woocommerce' ), esc_html( $order->get_shipping_method() ) ) . ''; } } else { echo '–'; } } /** * Renders the order total. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_order_total_column( WC_Order $order ): void { if ( $order->get_payment_method_title() ) { /* translators: %s: method */ echo '' . wp_kses_post( $order->get_formatted_order_total() ) . ''; } else { echo wp_kses_post( $order->get_formatted_order_total() ); } } /** * Renders order actions. * * @param WC_Order $order The order object for the current row. * * @return void */ public function render_wc_actions_column( WC_Order $order ): void { echo '

'; /** * Fires before the order action buttons (within the actions column for the order list table) * are registered. * * @param WC_Order $order Current order object. * @since 6.7.0 */ do_action( 'woocommerce_admin_order_actions_start', $order ); $actions = array(); if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) { $actions['processing'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Processing', 'woocommerce' ), 'action' => 'processing', ); } if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) { $actions['complete'] = array( 'url' => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ), 'name' => __( 'Complete', 'woocommerce' ), 'action' => 'complete', ); } /** * Provides an opportunity to modify the action buttons within the order list table. * * @param array $action Order actions. * @param WC_Order $order Current order object. * @since 6.7.0 */ $actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $order ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wc_render_action_buttons( $actions ); /** * Fires after the order action buttons (within the actions column for the order list table) * are rendered. * * @param WC_Order $order Current order object. * @since 6.7.0 */ do_action( 'woocommerce_admin_order_actions_end', $order ); echo '

'; } /** * Outputs hidden fields used to retain state when filtering. * * @return void */ private function print_hidden_form_fields(): void { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $state_params = array( 'paged', 'status', ); foreach ( $state_params as $param ) { if ( ! isset( $_GET[ $param ] ) ) { continue; } echo ''; } } /** * Gets the current action selected from the bulk actions dropdown. * * @return string|false The action name. False if no action was selected. */ public function current_action() { if ( ! empty( $_REQUEST['delete_all'] ) ) { return 'delete_all'; } return parent::current_action(); } /** * Handle bulk actions. */ public function handle_bulk_actions() { $action = $this->current_action(); if ( ! $action ) { return; } check_admin_referer( 'bulk-orders' ); $redirect_to = remove_query_arg( array( 'deleted', 'ids' ), wp_get_referer() ); $redirect_to = add_query_arg( 'paged', $this->get_pagenum(), $redirect_to ); if ( 'delete_all' === $action ) { // Get all trashed orders. $ids = wc_get_orders( array( 'type' => $this->order_type, 'status' => 'trash', 'limit' => -1, 'return' => 'ids', ) ); $action = 'delete'; } else { $ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array(); } /** * Allows 3rd parties to modify order IDs about to be affected by a bulk action. * * @param array Array of order IDs. */ $ids = apply_filters( // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment 'woocommerce_bulk_action_ids', $ids, $action, 'order' ); if ( ! $ids ) { wp_safe_redirect( $redirect_to ); exit; } $report_action = ''; $changed = 0; $action_handled = true; if ( 'remove_personal_data' === $action ) { $report_action = 'removed_personal_data'; $changed = $this->do_bulk_action_remove_personal_data( $ids ); } elseif ( 'trash' === $action ) { $changed = $this->do_delete( $ids ); $report_action = 'trashed'; } elseif ( 'delete' === $action ) { $changed = $this->do_delete( $ids, true ); $report_action = 'deleted'; } elseif ( 'untrash' === $action ) { $changed = $this->do_untrash( $ids ); $report_action = 'untrashed'; } elseif ( false !== strpos( $action, 'mark_' ) ) { $order_statuses = wc_get_order_statuses(); $new_status = substr( $action, 5 ); $report_action = 'marked_' . $new_status; if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { $changed = $this->do_bulk_action_mark_orders( $ids, $new_status ); } else { $action_handled = false; } } else { $action_handled = false; } // Custom action. if ( ! $action_handled ) { $screen = get_current_screen()->id; /** * This action is documented in /wp-admin/edit.php (it is a core WordPress hook). * * @since 7.2.0 * * @param string $redirect_to The URL to redirect to after processing the bulk actions. * @param string $action The current bulk action. * @param int[] $ids IDs for the orders to be processed. */ $custom_sendback = apply_filters( "handle_bulk_actions-{$screen}", $redirect_to, $action, $ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } if ( ! empty( $custom_sendback ) ) { $redirect_to = $custom_sendback; } elseif ( $changed ) { $redirect_to = add_query_arg( array( 'bulk_action' => $report_action, 'changed' => $changed, 'ids' => implode( ',', $ids ), ), $redirect_to ); } wp_safe_redirect( $redirect_to ); exit; } /** * Implements the "remove personal data" bulk action. * * @param array $order_ids The Order IDs. * @return int Number of orders modified. */ private function do_bulk_action_remove_personal_data( $order_ids ): int { $changed = 0; foreach ( $order_ids as $id ) { $order = wc_get_order( $id ); if ( ! $order ) { continue; } do_action( 'woocommerce_remove_order_personal_data', $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $changed++; } return $changed; } /** * Implements the "mark " bulk action. * * @param array $order_ids The order IDs to change. * @param string $new_status The new order status. * @return int Number of orders modified. */ private function do_bulk_action_mark_orders( $order_ids, $new_status ): int { $changed = 0; // Initialize payment gateways in case order has hooked status transition actions. WC()->payment_gateways(); foreach ( $order_ids as $id ) { $order = wc_get_order( $id ); if ( ! $order ) { continue; } $order->update_status( $new_status, __( 'Order status changed by bulk edit.', 'woocommerce' ), true ); do_action( 'woocommerce_order_edit_status', $id, $new_status ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $changed++; } return $changed; } /** * Handles bulk trashing of orders. * * @param int[] $ids Order IDs to be trashed. * @param bool $force_delete When set, the order will be completed deleted. Otherwise, it will be trashed. * * @return int Number of orders that were trashed. */ private function do_delete( array $ids, bool $force_delete = false ): int { $changed = 0; foreach ( $ids as $id ) { $order = wc_get_order( $id ); $order->delete( $force_delete ); $updated_order = wc_get_order( $id ); if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) { $changed++; } } return $changed; } /** * Handles bulk restoration of trashed orders. * * @param array $ids Order IDs to be restored to their previous status. * * @return int Number of orders that were restored from the trash. */ private function do_untrash( array $ids ): int { $orders_store = wc_get_container()->get( OrdersTableDataStore::class ); $changed = 0; foreach ( $ids as $id ) { if ( $orders_store->untrash_order( wc_get_order( $id ) ) ) { $changed++; } } return $changed; } /** * Show confirmation message that order status changed for number of orders. */ public function bulk_action_notices() { if ( empty( $_REQUEST['bulk_action'] ) ) { return; } $order_statuses = wc_get_order_statuses(); $number = absint( $_REQUEST['changed'] ?? 0 ); $bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); $message = ''; // Check if any status changes happened. foreach ( $order_statuses as $slug => $name ) { if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok. /* translators: %s: orders count */ $message = sprintf( _n( '%s order status changed.', '%s order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) ); break; } } switch ( $bulk_action ) { case 'removed_personal_data': /* translators: %s: orders count */ $message = sprintf( _n( 'Removed personal data from %s order.', 'Removed personal data from %s orders.', $number, 'woocommerce' ), number_format_i18n( $number ) ); echo '

' . esc_html( $message ) . '

'; break; case 'trashed': /* translators: %s: orders count */ $message = sprintf( _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) ); break; case 'untrashed': /* translators: %s: orders count */ $message = sprintf( _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $number, 'woocommerce' ), number_format_i18n( $number ) ); break; case 'deleted': /* translators: %s: orders count */ $message = sprintf( _n( '%s order permanently deleted.', '%s orders permanently deleted.', $number, 'woocommerce' ), number_format_i18n( $number ) ); break; } if ( ! empty( $message ) ) { echo '

' . esc_html( $message ) . '

'; } } /** * Enqueue list table scripts. * * @return void */ public function enqueue_scripts(): void { echo $this->get_order_preview_template(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_enqueue_script( 'wc-orders' ); } /** * Returns the HTML for the order preview template. * * @return string HTML template. */ public function get_order_preview_template(): string { $order_edit_url_placeholder = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ? esc_url( admin_url( 'admin.php?page=wc-orders&action=edit' ) ) . '&id={{ data.data.id }}' : esc_url( admin_url( 'post.php?action=edit' ) ) . '&post={{ data.data.id }}'; ob_start(); ?> orders_page_controller ) ) { $this->orders_page_controller = wc_get_container()->get( PageController::class ); } return $this->orders_page_controller; } /** * Setup hooks, actions and variables needed to render order edit page. * * @param \WC_Order $order Order object. */ public function setup( \WC_Order $order ) { $this->order = $order; $current_screen = get_current_screen(); $current_screen->is_block_editor( false ); $this->screen_id = $current_screen->id; if ( ! isset( $this->custom_meta_box ) ) { $this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class ); } if ( ! isset( $this->taxonomies_meta_box ) ) { $this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class ); } $this->add_save_meta_boxes(); $this->handle_order_update(); $this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) ); $this->add_order_specific_meta_box(); $this->add_order_taxonomies_meta_box(); /** * From wp-admin/includes/meta-boxes.php. * * Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here. * * @since 3.8.0. */ do_action( 'add_meta_boxes', $this->screen_id, $this->order ); /** * Provides an opportunity to inject custom meta boxes into the order editor screen. This * hook is an analog of `add_meta_boxes_` as provided by WordPress core. * * @since 7.4.0 * * @oaram WC_Order $order The order being edited. */ do_action( 'add_meta_boxes_' . $this->screen_id, $this->order ); $this->enqueue_scripts(); } /** * Set the current action for the form. * * @param string $action Action name. */ public function set_current_action( string $action ) { $this->current_action = $action; } /** * Hooks meta box for order specific meta. */ private function add_order_specific_meta_box() { add_meta_box( 'order_custom', __( 'Custom Fields', 'woocommerce' ), array( $this, 'render_custom_meta_box' ), $this->screen_id, 'normal' ); } /** * Render custom meta box. * * @return void */ private function add_order_taxonomies_meta_box() { $this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() ); } /** * Register order attribution meta boxes if the feature is enabled. * * @since 8.5.0 * * @param string $screen_id Screen ID. * @param string $title Title of the page. * * @return void */ private static function maybe_register_order_attribution( string $screen_id, string $title ) { /** * Features controller. * * @var FeaturesController $feature_controller */ $feature_controller = wc_get_container()->get( FeaturesController::class ); if ( ! $feature_controller->feature_is_enabled( 'order_attribution' ) ) { return; } /** * Order attribution meta box. * * @var OrderAttribution $order_attribution_meta_box */ $order_attribution_meta_box = wc_get_container()->get( OrderAttribution::class ); add_meta_box( 'woocommerce-order-source-data', /* Translators: %s order type name. */ sprintf( __( '%s attribution', 'woocommerce' ), $title ), function( $post_or_order ) use ( $order_attribution_meta_box ) { $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order ); if ( $order instanceof WC_Order ) { $order_attribution_meta_box->output( $order ); } }, $screen_id, 'side', 'high' ); /** * Customer history meta box. * * @var CustomerHistory $customer_history_meta_box */ $customer_history_meta_box = wc_get_container()->get( CustomerHistory::class ); add_meta_box( 'woocommerce-customer-history', __( 'Customer history', 'woocommerce' ), function( $post_or_order ) use ( $customer_history_meta_box ) { $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order ); if ( $order instanceof WC_Order ) { $customer_history_meta_box->output( $order ); } }, $screen_id, 'side', 'high' ); } /** * Takes care of updating order data. Fires action that metaboxes can hook to for order data updating. * * @return void */ public function handle_order_update() { if ( ! isset( $this->order ) ) { return; } if ( 'edit_order' !== sanitize_text_field( wp_unslash( $_POST['action'] ?? '' ) ) ) { return; } check_admin_referer( $this->get_order_edit_nonce_action() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object. $taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null; $this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input ); /** * Save meta for shop order. * * @param int Order ID. * @param \WC_Order Post object. * * @since 2.1.0 */ do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order ); $this->custom_meta_box->handle_metadata_changes($this->order); // Order updated message. $this->message = 1; // Claim lock. $edit_lock = wc_get_container()->get( EditLock::class ); $edit_lock->lock( $this->order ); $this->redirect_order( $this->order ); } /** * Helper method to redirect to order edit page. * * @since 8.0.0 * * @param \WC_Order $order Order object. */ private function redirect_order( \WC_Order $order ) { $redirect_to = $this->get_page_controller()->get_edit_url( $order->get_id() ); if ( isset( $this->message ) ) { $redirect_to = add_query_arg( 'message', $this->message, $redirect_to ); } wp_safe_redirect( /** * Filter the URL used to redirect after an order is updated. Similar to the WP post's `redirect_post_location` filter. * * @param string $redirect_to The redirect destination URL. * @param int $order_id The order ID. * @param \WC_Order $order The order object. * * @since 8.0.0 */ apply_filters( 'woocommerce_redirect_order_location', $redirect_to, $order->get_id(), $order ) ); exit; } /** * Helper method to get the name of order edit nonce. * * @return string Nonce action name. */ private function get_order_edit_nonce_action() { return 'update-order_' . $this->order->get_id(); } /** * Render meta box for order specific meta. */ public function render_custom_meta_box() { $this->custom_meta_box->output( $this->order ); } /** * Render order edit page. */ public function display() { /** * This is used by the order edit page to show messages in the notice fields. * It should be similar to post_updated_messages filter, i.e.: * array( * {order_type} => array( * 1 => 'Order updated.', * 2 => 'Custom field updated.', * ... * ). * * The index to be displayed is computed from the $_GET['message'] variable. * * @since 7.4.0. */ $messages = apply_filters( 'woocommerce_order_updated_messages', array() ); $message = $this->message; if ( isset( $_GET['message'] ) ) { $message = absint( $_GET['message'] ); } if ( isset( $message ) ) { $message = $messages[ $this->order->get_type() ][ $message ] ?? false; } $this->render_wrapper_start( '', $message ); $this->render_meta_boxes(); $this->render_wrapper_end(); } /** * Helper function to render wrapper start. * * @param string $notice Notice to display, if any. * @param string $message Message to display, if any. */ private function render_wrapper_start( $notice = '', $message = '' ) { $post_type = get_post_type_object( $this->order->get_type() ); $edit_page_url = $this->get_page_controller()->get_edit_url( $this->order->get_id() ); $form_action = 'edit_order'; $referer = wp_get_referer(); $new_page_url = $this->get_page_controller()->get_new_page_url( $this->order->get_type() ); ?>

current_action ? esc_html( $post_type->labels->add_new_item ) : esc_html( $post_type->labels->edit_item ); ?>

current_action ) { echo ' ' . esc_html( $post_type->labels->add_new ) . ''; } ?>

order ); ?> > get_order_edit_nonce_action() ); ?> order ); wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
screen_id, 'side', $this->order ); ?>
screen_id, 'normal', $this->order ); do_meta_boxes( $this->screen_id, 'advanced', $this->order ); ?>
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.php000064400000017016150732355220010222 0ustar00get_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' ); ?>
ID, 64 ); ?>

display_name ) ) ); ?>


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.php000064400000001425150732355220016200 0ustar00id = static::GATEWAY_ID; $this->method_title = $wc_pay_spec->title; if ( property_exists( $wc_pay_spec, 'sub_title' ) ) { $this->title = sprintf( '%s', $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.php000064400000012436150732355220011064 0ustar00plugins ) && 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.php000064400000021702150732355220007212 0ustar00 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' ), '', '' ), '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.php000064400000044743150732355220012675 0ustar00 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.php000064400000017357150732355220012436 0ustar00file_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'] ?? '' ) ); ?>
'logs-filter-submit', ) ); ?>
_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( '

%s

', 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' => '', '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(); ?> 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( ' – %d', $item->get_rotation() ); } return sprintf( '%2$s%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.php000064400000007341150732355220012353 0ustar00path = $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.php000064400000013173150732355220012754 0ustar00file_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( '

%s

', esc_html( $total_items->get_error_message() ) ); return; } if ( $total_items >= $this->file_controller::SEARCH_MAX_RESULTS ) { printf( '

%s

', sprintf( // translators: %s is a number. esc_html__( 'The number of search results has reached the limit of %s. Try refining your search.', 'woocommerce' ), esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_RESULTS ) ) ) ); } $total_pages = ceil( $total_items / $per_page ); $results = $this->file_controller->search_within_files( $search, $args, $file_args ); $this->items = $results; $this->set_pagination_args( array( 'per_page' => $per_page, 'total_items' => $total_items, 'total_pages' => $total_pages, ) ); } /** * Gets a list of columns. * * @return array */ public function get_columns(): array { $columns = array( 'file_id' => esc_html__( 'File', 'woocommerce' ), 'line_number' => esc_html__( 'Line #', 'woocommerce' ), 'line' => esc_html__( 'Matched Line', 'woocommerce' ), ); return $columns; } /** * Render the file_id column. * * @param array $item The current search result being rendered. * * @return string */ public function column_file_id( array $item ): string { // Add a word break after the rotation number, if it exists. $file_id = preg_replace( '/\.([0-9])+\-/', '.\1-', $item['file_id'] ); return wp_kses( $file_id, array( 'wbr' => array() ) ); } /** * Render the line_number column. * * @param array $item The current search result being rendered. * * @return string */ public function column_line_number( array $item ): string { $match_url = add_query_arg( array( 'view' => 'single_file', 'file_id' => $item['file_id'], ), $this->page_controller->get_logs_tab_url() . '#L' . absint( $item['line_number'] ) ); return sprintf( '%2$s', esc_url( $match_url ), sprintf( // translators: %s is a line number in a file. esc_html__( 'Line %s', 'woocommerce' ), number_format_i18n( absint( $item['line_number'] ) ) ) ); } /** * Render the line column. * * @param array $item The current search result being rendered. * * @return string */ public function column_line( array $item ): string { $params = $this->page_controller->get_query_params( array( 'search' ) ); $line = $item['line']; // Highlight matches within the line. $pattern = preg_quote( $params['search'], '/' ); preg_match_all( "/$pattern/i", $line, $matches, PREG_OFFSET_CAPTURE ); if ( is_array( $matches[0] ) && count( $matches[0] ) >= 1 ) { $length_change = 0; foreach ( $matches[0] as $match ) { $replace = '' . $match[0] . ''; $offset = $match[1] + $length_change; $orig_length = strlen( $match[0] ); $replace_length = strlen( $replace ); $line = substr_replace( $line, $replace, $offset, $orig_length ); $length_change += $replace_length - $orig_length; } } return wp_kses_post( $line ); } /** * Helper to get the default value for the per_page arg. * * @return int */ public function get_per_page_default(): int { return $this->file_controller::DEFAULTS_SEARCH_WITHIN_FILES['per_page']; } } Logging/FileV2/File.php000064400000031755150732355220010630 0ustar00path = $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.php000064400000014414150732355220011702 0ustar00file_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.php000064400000045411150732355220011574 0ustar00file_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(); ?>

render_search_field(); ?>
$value ) : ?> display(); ?>
get_query_params( array( 'file_id', 'view' ) ); $file = $this->file_controller->get_file_by_id( $params['file_id'] ); if ( is_wp_error( $file ) ) { ?>
get_error_message() ) ); ?> %2$s

', esc_url( $this->get_logs_tab_url() ), esc_html__( 'Return to the file list.', 'woocommerce' ) ); ?>
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; ?>

%s', esc_html( $file->get_file_id() ) ) ); ?>

1 ) : ?>
%2$s', esc_url( $download_url ), esc_html__( 'Download', 'woocommerce' ) ); ?> %3$s', esc_url( $delete_url ), esc_attr( $delete_confirmation_js ), esc_html__( 'Delete permanently', 'woocommerce' ) ); ?>
format_line( $line, $line_number ); $line_number ++; } ?>
get_query_params( array( 'order', 'orderby', 'search', 'source', 'view' ) ); $list_table = $this->get_list_table( $params['view'] ); $list_table->prepare_items(); ?>

render_search_field(); ?>
display(); ?> '', '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 ) { ?>

%s', $segments[0] ); $has_timestamp = true; } if ( isset( $segments[1] ) && in_array( strtolower( $segments[1] ), $severity_levels, true ) ) { $segments[1] = sprintf( '%2$s', 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( '
%1$s
%2$s
', 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( '%3$s%4$s', absint( $line_number ), esc_attr( $classes ), sprintf( '', absint( $line_number ) ), sprintf( '%s', 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 ) { ?> 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.php000064400000012544150732355220012127 0ustar00 '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.php000064400000010521150732355220012414 0ustar00add_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.php000064400000002675150732355220016527 0ustar00block_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.php000064400000003213150732355220016016 0ustar00get_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.php000064400000002575150732355220015547 0ustar00id = $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.php000064400000001400150732355220012713 0ustar00get_root_template(), $this ); return $this->add_inner_block( $block ); } } BlockTemplates/BlockFormattedTemplateTrait.php000064400000003335150732355220015576 0ustar00get_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.php000064400000021012150732355220012704 0ustar00validate( $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.php000064400000024122150732355220014074 0ustar00get_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.php000064400000006152150732355220014410 0ustar00block_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.php000064400000034070150732355220014064 0ustar00 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' ), 'BlockTemplateLogger::log', $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.php000064400000001356150732355220011231 0ustar00get_root_template(), $this ); return $this->add_inner_block( $block ); } } Settings.php000064400000031370150732355220007065 0ustar00 $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.php000064400000001674150732355220010267 0ustar00jetpack_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.php000064400000037067150732355220010432 0ustar00is_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 = ' 1'; 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.php000064400000004233150732355220010710 0ustar00 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.php000064400000004502150732355220007314 0ustar00 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.php000064400000036407150732355220007740 0ustar00handle, $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 '', "\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' ); } } }