Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/Internal.zip
Назад
PK �[1]L��.� � Integrations/WPConsentAPI.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\Integrations; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\Traits\ScriptDebug; use WP_CONSENT_API; /** * Class WPConsentAPI * * @since 8.5.0 */ class WPConsentAPI { use ScriptDebug; /** * Identifier of the consent category used for order attribution. * * @var string */ public static $consent_category = 'marketing'; /** * Register the consent API. * * @return void */ public function register() { add_action( 'init', function() { $this->on_init(); }, 20 // After OrderAttributionController. ); } /** * Register our hooks on init. * * @return void */ protected function on_init() { // Include integration to WP Consent Level API if available. if ( ! $this->is_wp_consent_api_active() ) { return; } $plugin = plugin_basename( WC_PLUGIN_FILE ); add_filter( "wp_consent_api_registered_{$plugin}", '__return_true' ); add_action( 'wp_enqueue_scripts', function() { $this->enqueue_consent_api_scripts(); } ); /** * Modify the "allowTracking" flag consent if the user has consented to marketing. * * Wp-consent-api will initialize the modules on "init" with priority 9, * So this code needs to be run after that. */ add_filter( 'wc_order_attribution_allow_tracking', function() { return function_exists( 'wp_has_consent' ) && wp_has_consent( self::$consent_category ); } ); } /** * Check if WP Cookie Consent API is active * * @return bool */ protected function is_wp_consent_api_active() { return class_exists( WP_CONSENT_API::class ); } /** * Enqueue JS for integration with WP Consent Level API * * @return void */ private function enqueue_consent_api_scripts() { wp_enqueue_script( 'wp-consent-api-integration', plugins_url( "assets/js/frontend/wp-consent-api-integration{$this->get_script_suffix()}.js", WC_PLUGIN_FILE ), array( 'wp-consent-api', 'wc-order-attribution' ), Constants::get_constant( 'WC_VERSION' ), true ); // Add data for the script above. `wp_enqueue_script` API does not allow data attributes, // so we need a separate script tag and pollute the global scope. wp_add_inline_script( 'wp-consent-api-integration', sprintf( 'window.wc_order_attribution.params.consentCategory = %s;', wp_json_encode( self::$consent_category, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ), 'before' ); } } PK �[1]�E~<� � Integrations/WPPostsImporter.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\Integrations; /** * Class WPPostsImporter * * @since 10.1.0 */ class WPPostsImporter { /** * Register the WP Posts importer. * * @return void */ public function register() { add_action( 'wp_import_posts', array( $this, 'register_product_attribute_taxonomies' ), 100, 1 ); } /** * Register product attribute taxonomies when importing posts via the WXR importer. * * @since 10.1.0 * * @param array $posts The posts to process. * @return array */ public function register_product_attribute_taxonomies( $posts ) { if ( ! is_array( $posts ) || empty( $posts ) ) { return $posts; } foreach ( $posts as $post ) { if ( 'product' !== $post['post_type'] || empty( $post['terms'] ) ) { continue; } foreach ( $post['terms'] as $term ) { if ( ! strstr( $term['domain'], 'pa_' ) ) { continue; } if ( taxonomy_exists( $term['domain'] ) ) { continue; } $attribute_name = wc_attribute_taxonomy_slug( $term['domain'] ); // Create the taxonomy. if ( ! in_array( $attribute_name, wc_get_attribute_taxonomies(), true ) ) { wc_create_attribute( array( 'name' => $attribute_name, 'slug' => $attribute_name, 'type' => 'select', 'order_by' => 'menu_order', 'has_archives' => false, ) ); } // Register the taxonomy so that the import works. register_taxonomy( $term['domain'], // phpcs:ignore apply_filters( 'woocommerce_taxonomy_objects_' . $term['domain'], array( 'product' ) ), // phpcs:ignore apply_filters( 'woocommerce_taxonomy_args_' . $term['domain'], array( 'hierarchical' => true, 'show_ui' => false, 'query_var' => true, 'rewrite' => false, ) ) ); } } return $posts; } } PK �[1]���|� � Utilities/URLException.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; use Exception; /** * Used to represent a problem encountered when processing a URL. */ class URLException extends Exception {} PK �[1]�_F8 8 Utilities/PluginInstaller.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; use Automattic\WooCommerce\Internal\RegisterHooksInterface; use Automattic\WooCommerce\Utilities\{ PluginUtil, StringUtil }; /** * This class allows installing a plugin programmatically. * * Information about plugins installed in that way will be stored in a 'woocommerce_autoinstalled_plugins' option, * and a notice will be shown under the plugin name in the plugins list indicating that it was automatically * installed (these notices can be disabled with the 'woocommerce_show_autoinstalled_plugin_notices' hook). * * Currently it's only possible to install new plugins, not to upgrade or reinstall already installed plugins. * * The 'upgrader_process_complete' hook is used to remove the autoinstall information from any plugin that is later * upgraded or reinstalled by any means other than the usage of this class. */ class PluginInstaller implements RegisterHooksInterface { /** * Flag indicating that a plugin install is in progress, so the upgrader_process_complete hook must be ignored. * * @var bool */ private bool $installing_plugin = false; /** * Attach hooks used by the class. */ public function register() { add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 ); add_action( 'upgrader_process_complete', array( $this, 'handle_upgrader_process_complete' ), 10, 2 ); } /** * Programmatically installs a plugin. Upgrade/reinstall of already existing plugins is not supported. * The plugin source must be the WordPress.org plugins directory. * * $metadata can contain anything, but the following keys are recognized by the code that renders the notice * in the plugins list: * * - 'installed_by': defaults to 'WooCommerce' if not present. * - 'info_link': if present, a "More information" link will be included in the notice. * * If 'installed_by' is supplied and it's not 'WooCommerce' (case-insensitive), an exception will be thrown * if the code calling this method is not in a WooCommerce core file (in 'includes' or in 'src'). * * Information about plugins successfully installed with this method will be kept in an option named * 'woocommerce_autoinstalled_plugins'. Keys will be the plugin name and values will be associative arrays * with these keys: 'plugin_name', 'version', 'date' and 'metadata' (same meaning as in the returned array). * * A log entry will be created with the result of the process and all the installer messages * (source: 'plugin_auto_installs'). In multisite this log entry will be created on each site. * * The returned array will contain the following (only 'install_ok' and 'messages' if the installation fails): * * - 'install_ok', a boolean. * - 'messages', all the messages generated by the installer. * - 'plugin_name', in the form of 'directory/file.php' (taken from the instance of PluginInstaller used). * - 'version', of the plugin that has been installed. * - 'date', ISO-formatted installation date. * - 'metadata', as supplied (except the 'plugin_name' key) and only if not empty. * * If the plugin is already in the process of being installed (can happen in multisite), the returned array * will contain only one key: 'already_installing', with a value of true. * * @param string $plugin_url URL or file path of the plugin to install. * @param array $metadata Metadata to store if the installation succeeds. * @return array Information about the installation result. * @throws \InvalidArgumentException Source doesn't start with 'https://downloads.wordpress.org/', or installer name is 'WooCommerce' but caller is not WooCommerce core code. */ public function install_plugin( string $plugin_url, array $metadata = array() ): array { $this->installing_plugin = true; $plugins_being_installed = get_site_option( 'woocommerce_autoinstalling_plugins', array() ); if ( in_array( $plugin_url, $plugins_being_installed, true ) ) { return array( 'already_installing' => true ); } $plugins_being_installed[] = $plugin_url; update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed ); try { return $this->install_plugin_core( $plugin_url, $metadata ); } finally { $plugins_being_installed = array_diff( $plugins_being_installed, array( $plugin_url ) ); if ( empty( $plugins_being_installed ) ) { delete_site_option( 'woocommerce_autoinstalling_plugins' ); } else { update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed ); } $this->installing_plugin = false; } } /** * Core version of 'install_plugin' (it doesn't handle the $installing_plugin flag). * * @param string $plugin_url URL or file path of the plugin to install. * @param array $metadata Metadata to store if the installation succeeds. * @return array Information about the installation result. * @throws \InvalidArgumentException Source doesn't start with 'https://downloads.wordpress.org/', or installer name is 'WooCommerce' but caller is not WooCommerce core code. */ private function install_plugin_core( string $plugin_url, array $metadata ): array { if ( ! StringUtil::starts_with( $plugin_url, 'https://downloads.wordpress.org/', false ) ) { throw new \InvalidArgumentException( "Only installs from the WordPress.org plugins directory (plugin URL starting with 'https://downloads.wordpress.org/') are allowed." ); } $installed_by = $metadata['installed_by'] ?? 'WooCommerce'; if ( 0 === strcasecmp( 'WooCommerce', $installed_by ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $calling_file = StringUtil::normalize_local_path_slashes( debug_backtrace()[1]['file'] ?? '' ); // [1], not [0], because the immediate caller is the install_plugin method. if ( ! StringUtil::starts_with( $calling_file, StringUtil::normalize_local_path_slashes( WC_ABSPATH . 'includes/' ) ) && ! StringUtil::starts_with( $calling_file, StringUtil::normalize_local_path_slashes( WC_ABSPATH . 'src/' ) ) ) { throw new \InvalidArgumentException( "If the value of 'installed_by' is 'WooCommerce', the caller of the method must be a WooCommerce core class or function." ); } } if ( ! class_exists( \Automatic_Upgrader_Skin::class ) ) { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php'; include_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php'; } $skin = new \Automatic_Upgrader_Skin(); if ( ! class_exists( \Plugin_Upgrader::class ) ) { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } $upgrader = new \Plugin_Upgrader( $skin ); $install_ok = $upgrader->install( $plugin_url ); $result = array( 'messages' => $skin->get_upgrade_messages() ); if ( $install_ok ) { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugin_name = $upgrader->plugin_info(); $plugin_version = get_plugins()[ $plugin_name ]['Version']; $result['plugin_name'] = $plugin_name; $plugin_data = array( 'version' => $plugin_version, 'date' => current_time( 'mysql' ), ); if ( ! empty( $metadata ) ) { $plugin_data['metadata'] = $metadata; } $auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins', array() ); $auto_installed_plugins[ $plugin_name ] = $plugin_data; update_site_option( 'woocommerce_autoinstalled_plugins', $auto_installed_plugins ); $auto_installed_plugins_history = get_site_option( 'woocommerce_history_of_autoinstalled_plugins', array() ); if ( ! isset( $auto_installed_plugins_history[ $plugin_name ] ) ) { $auto_installed_plugins_history[ $plugin_name ] = $plugin_data; update_site_option( 'woocommerce_history_of_autoinstalled_plugins', $auto_installed_plugins_history ); } $post_install = function () use ( $plugin_name, $plugin_version, $installed_by, $plugin_url, $plugin_data ) { $log_context = array( 'source' => 'plugin_auto_installs', 'recorded_data' => $plugin_data, ); wc_get_logger()->info( "Plugin $plugin_name v{$plugin_version} installed by $installed_by, source: $plugin_url", $log_context ); }; } else { $messages = $skin->get_upgrade_messages(); $post_install = function () use ( $plugin_url, $installed_by, $messages ) { $log_context = array( 'source' => 'plugin_auto_installs', 'installer_messages' => $messages, ); wc_get_logger()->error( "$installed_by failed to install plugin from source: $plugin_url", $log_context ); }; } if ( is_multisite() ) { // We log the install in the main site, unless the main site doesn't have WooCommerce installed; // in that case we fallback to logging in the current site. switch_to_blog( get_main_site_id() ); if ( self::woocommerce_is_active_in_current_site() ) { $post_install(); restore_current_blog(); } else { restore_current_blog(); $post_install(); } } else { $post_install(); } $result['install_ok'] = $install_ok ?? false; return $result; } /** * Check if WooCommerce is installed and active in the current blog. * This is useful for multisite installs when a blog other than the one running this code is selected with 'switch_to_blog'. * * @return bool True if WooCommerce is installed and active in the current blog, false otherwise. */ private static function woocommerce_is_active_in_current_site(): bool { $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins(); return ! empty( array_filter( $active_valid_plugins, fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 ) ); } /** * Handler for the 'plugin_list_rows' hook, it will display a notice under the name of the plugins * that have been installed using this class (unless the 'woocommerce_show_autoinstalled_plugin_notices' filter * returns false) in the plugins list page. * * @param string $plugin_file Name of the plugin. * @param array $plugin_data Plugin data. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function handle_plugin_list_rows( $plugin_file, $plugin_data ) { global $wp_list_table; if ( is_null( $wp_list_table ) ) { return; } /** * Filter to suppress the notice about autoinstalled plugins in the plugins list page. * * @since 8.8.0 * * @param bool $display_notice Whether notices should be displayed or not. * @returns bool */ if ( ! apply_filters( 'woocommerce_show_autoinstalled_plugin_notices', '__return_true' ) ) { return; } $auto_installed_plugins_info = get_site_option( 'woocommerce_autoinstalled_plugins', array() ); $current_plugin_info = $auto_installed_plugins_info[ $plugin_file ] ?? null; if ( is_null( $current_plugin_info ) || $current_plugin_info['version'] !== $plugin_data['Version'] ) { return; } $installed_by = $current_plugin_info['metadata']['installed_by'] ?? 'WooCommerce'; $info_link = $current_plugin_info['metadata']['info_link'] ?? null; if ( $info_link ) { /* translators: 1 = who installed the plugin, 2 = ISO-formatted date and time, 3 = URL */ $message = sprintf( __( 'Plugin installed by %1$s on %2$s. <a target="_blank" href="%3$s">More information</a>', 'woocommerce' ), $installed_by, $current_plugin_info['date'], $info_link ); } else { /* translators: 1 = who installed the plugin, 2 = ISO-formatted date and time */ $message = sprintf( __( 'Plugin installed by %1$s on %2$s.', 'woocommerce' ), $installed_by, $current_plugin_info['date'] ); } $columns_count = $wp_list_table->get_column_count(); $is_active = is_plugin_active( $plugin_file ); $is_active_class = $is_active ? 'active' : 'inactive'; $is_active_td_style = $is_active ? "style='border-left: 4px solid #72aee6;'" : ''; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped ?> <tr class='plugin-update-tr update <?php echo $is_active_class; ?>' data-plugin='<?php echo $plugin_file; ?>' data-plugin-row-type='feature-incomp-warn'> <td colspan='<?php echo $columns_count; ?>' class='plugin-update'<?php echo $is_active_td_style; ?>> <div class='notice inline notice-success notice-alt'> <p> ℹ️ <?php echo $message; ?> </p> </div> </td> </tr> <?php // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Handler for the 'upgrader_process_complete' hook. It's used to remove the autoinstalled plugin information * for plugins that are upgraded or reinstalled manually (or more generally, by using any install method * other than this class). * * @param \WP_Upgrader $upgrader The upgrader class that has performed the plugin upgrade/reinstall. * @param array $hook_extra Extra information about the upgrade process. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function handle_upgrader_process_complete( \WP_Upgrader $upgrader, array $hook_extra ) { if ( $this->installing_plugin || ! ( $upgrader instanceof \Plugin_Upgrader ) || ( 'plugin' !== ( $hook_extra['type'] ?? null ) ) ) { return; } $auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins' ); if ( ! $auto_installed_plugins ) { return; } if ( $hook_extra['bulk'] ?? false ) { $updated_plugin_names = $hook_extra['plugins'] ?? array(); } else { $updated_plugin_names = array( $upgrader->plugin_info() ); } $auto_installed_plugin_names = array_keys( $auto_installed_plugins ); $updated_auto_installed_plugin_names = array_intersect( $auto_installed_plugin_names, $updated_plugin_names ); if ( empty( $updated_auto_installed_plugin_names ) ) { return; } $new_auto_installed_plugins = array_diff_key( $auto_installed_plugins, array_flip( $updated_auto_installed_plugin_names ) ); if ( empty( $new_auto_installed_plugins ) ) { delete_site_option( 'woocommerce_autoinstalled_plugins' ); } else { update_site_option( 'woocommerce_autoinstalled_plugins', $new_auto_installed_plugins ); } } } PK �[1]R�`� � Utilities/FilesystemUtil.phpnu ��� <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Utilities; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Proxies\LegacyProxy; use Exception; use WP_Filesystem_Base; /** * FilesystemUtil class. */ class FilesystemUtil { /** * Wrapper to retrieve the class instance contained in the $wp_filesystem global, after initializing if necessary. * * @return WP_Filesystem_Base * @throws Exception Thrown when the filesystem fails to initialize. */ public static function get_wp_filesystem(): WP_Filesystem_Base { global $wp_filesystem; if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { $initialized = self::initialize_wp_filesystem(); if ( false === $initialized ) { throw new Exception( 'The WordPress filesystem could not be initialized.' ); } } return $wp_filesystem; } /** * Get the WP filesystem method, with a fallback to 'direct' if no FS_METHOD constant exists and there are not FTP related options/credentials set. * * @return string|false The name of the WP filesystem method to use. */ public static function get_wp_filesystem_method_or_direct() { $proxy = wc_get_container()->get( LegacyProxy::class ); if ( ! self::constant_exists( 'FS_METHOD' ) && false === $proxy->call_function( 'get_option', 'ftp_credentials' ) && ! self::constant_exists( 'FTP_HOST' ) ) { return 'direct'; } $method = $proxy->call_function( 'get_filesystem_method' ); if ( $method ) { return $method; } return 'direct'; } /** * Check if a constant exists and is not null. * * @param string $name Constant name. * @return bool True if the constant exists and its value is not null. */ private static function constant_exists( string $name ): bool { return Constants::is_defined( $name ) && ! is_null( Constants::get_constant( $name ) ); } /** * Recursively creates a directory (if it doesn't exist) and adds an empty index.html and a .htaccess to prevent * directory listing. * * @since 9.3.0 * * @param string $path Directory to create. * @param bool $allow_file_access Whether to allow file access while preventing directory listing. Default false (deny all access). * @throws \Exception In case of error. */ public static function mkdir_p_not_indexable( string $path, bool $allow_file_access = false ): void { $wp_fs = self::get_wp_filesystem(); if ( $wp_fs->is_dir( $path ) ) { return; } if ( ! wp_mkdir_p( $path ) ) { throw new \Exception( esc_html( sprintf( 'Could not create directory: %s.', wp_basename( $path ) ) ) ); } $htaccess_content = $allow_file_access ? 'Options -Indexes' : 'deny from all'; $files = array( '.htaccess' => $htaccess_content, 'index.html' => '', ); foreach ( $files as $name => $content ) { $wp_fs->put_contents( trailingslashit( $path ) . $name, $content ); } } /** * Wrapper to initialize the WP filesystem with defined credentials if they are available. * * @return bool True if the $wp_filesystem global was successfully initialized. */ protected static function initialize_wp_filesystem(): bool { global $wp_filesystem; if ( $wp_filesystem instanceof WP_Filesystem_Base ) { return true; } require_once ABSPATH . 'wp-admin/includes/file.php'; $method = self::get_wp_filesystem_method_or_direct(); $initialized = false; if ( 'direct' === $method ) { $initialized = WP_Filesystem(); } elseif ( false !== $method ) { // See https://core.trac.wordpress.org/changeset/56341. ob_start(); $credentials = request_filesystem_credentials( '' ); ob_end_clean(); $initialized = $credentials && WP_Filesystem( $credentials ); } return is_null( $initialized ) ? false : $initialized; } /** * Validate that a file path is a valid upload path. * * @param string $path The path to validate. * @throws \Exception If the file path is not a valid upload path. */ public static function validate_upload_file_path( string $path ): void { $wp_filesystem = self::get_wp_filesystem(); // File must exist and be readable. $is_valid_file = $wp_filesystem->is_readable( $path ); // Check that file is within an allowed location. if ( $is_valid_file ) { $is_valid_file = self::file_is_in_directory( $path, $wp_filesystem->abspath() ); if ( ! $is_valid_file ) { $upload_dir = wp_get_upload_dir(); $is_valid_file = false === $upload_dir['error'] && self::file_is_in_directory( $path, $upload_dir['basedir'] ); } } if ( ! $is_valid_file ) { throw new \Exception( esc_html__( 'File path is not a valid upload path.', 'woocommerce' ) ); } } /** * Check if a given file is inside a given directory. * * @param string $file_path The full path of the file to check. * @param string $directory The path of the directory to check. * @return bool True if the file is inside the directory. */ private static function file_is_in_directory( string $file_path, string $directory ): bool { // Extract protocol if it exists. $protocol = ''; if ( preg_match( '#^([a-z0-9]+://)#i', $file_path, $matches ) ) { $protocol = $matches[1]; $file_path = preg_replace( '#^[a-z0-9]+://#i', '', $file_path ); } $file_path = (string) new URL( $file_path ); // This resolves '/../' sequences. $file_path = preg_replace( '/^file:\\/\\//', $protocol, $file_path ); $file_path = preg_replace( '/^file:\\/\\//', '', $file_path ); return 0 === stripos( wp_normalize_path( $file_path ), trailingslashit( wp_normalize_path( $directory ) ) ); } } PK �[1]�,~� � Utilities/LegacyRestApiStub.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; use Automattic\WooCommerce\Internal\RegisterHooksInterface; use Automattic\WooCommerce\Utilities\RestApiUtil; /** * The Legacy REST API was removed in WooCommerce 9.0 and is now available as a dedicated extension. * A stub is kept in WooCommerce core that acts when the extension is not installed and has two purposes: * * 1. Return a "The WooCommerce API is disabled on this site" error for any request to the Legacy REST API endpoints. * * 2. Provide the not-endpoint related utility methods that were previously supplied by the WC_API class, * this is achieved by setting the value of WooCommerce::api (typically accessed via 'WC()->api') to an instance of this class. * * DO NOT add any additional public method to this class unless the method existed with the same signature in the old WC_API class. * * See: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ */ class LegacyRestApiStub implements RegisterHooksInterface { /** * The instance of RestApiUtil to use. * * @var RestApiUtil */ private RestApiUtil $rest_api_util; /** * Set up the Legacy REST API endpoints stub. */ public function register() { add_action( 'init', array( __CLASS__, 'add_rewrite_rules_for_legacy_rest_api_stub' ), 0 ); add_action( 'query_vars', array( __CLASS__, 'add_query_vars_for_legacy_rest_api_stub' ), 0 ); add_action( 'parse_request', array( __CLASS__, 'parse_legacy_rest_api_request' ), 0 ); } /** * Initialize the class dependencies. * * @internal * @param RestApiUtil $rest_api_util The instance of RestApiUtil to use. */ final public function init( RestApiUtil $rest_api_util ) { $this->rest_api_util = $rest_api_util; } /** * Add the necessary rewrite rules for the Legacy REST API * (either the dedicated extension if it's installed, or the stub otherwise). * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public static function add_rewrite_rules_for_legacy_rest_api_stub() { add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' ); add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' ); add_rewrite_endpoint( 'wc-api', EP_ALL ); } /** * Add the necessary request query variables for the Legacy REST API * (either the dedicated extension if it's installed, or the stub otherwise). * * @param array $vars The query variables array to extend. * @return array The extended query variables array. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public static function add_query_vars_for_legacy_rest_api_stub( $vars ) { $vars[] = 'wc-api-version'; $vars[] = 'wc-api-route'; $vars[] = 'wc-api'; return $vars; } /** * Process an incoming request for the Legacy REST API. * * If the dedicated Legacy REST API extension is installed and active, this method does nothing. * Otherwise it returns a "The WooCommerce API is disabled on this site" error, * unless the request contains a "wc-api" variable and the appropriate * "woocommerce_api_*" hook is set. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public static function parse_legacy_rest_api_request() { global $wp; // The WC_Legacy_REST_API_Plugin class existence means that the Legacy REST API extension is installed and active. if ( class_exists( 'WC_Legacy_REST_API_Plugin' ) ) { return; } self::maybe_process_wc_api_query_var(); // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput if ( ! empty( $_GET['wc-api-version'] ) ) { $wp->query_vars['wc-api-version'] = $_GET['wc-api-version']; } if ( ! empty( $_GET['wc-api-route'] ) ) { $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; } if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { header( sprintf( 'Content-Type: %s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) ) ); status_header( 404 ); echo wp_json_encode( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site', ), ) ); exit; } // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput } /** * Process a "wc-api" variable if present in the query, by triggering the appropriate hooks. */ private static function maybe_process_wc_api_query_var() { global $wp; // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['wc-api'] ) ) { $wp->query_vars['wc-api'] = sanitize_key( wp_unslash( $_GET['wc-api'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended // wc-api endpoint requests. if ( ! empty( $wp->query_vars['wc-api'] ) ) { // Buffer, we won't want any output here. ob_start(); // No cache headers. wc_nocache_headers(); // Clean the API request. $api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) ); // Make sure gateways are available for request. WC()->payment_gateways(); // phpcs:disable WooCommerce.Commenting.CommentHooks.HookCommentWrongStyle // Trigger generic action before request hook. do_action( 'woocommerce_api_request', $api_request ); // Is there actually something hooked into this API request? If not trigger 400 - Bad request. status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 ); // Trigger an action which plugins can hook into to fulfill the request. do_action( 'woocommerce_api_' . $api_request ); // phpcs:enable WooCommerce.Commenting.CommentHooks.HookCommentWrongStyle // Done, clear buffer and exit. ob_end_clean(); die( '-1' ); } } /** * Get data from a WooCommerce API endpoint. * This method used to be part of the WooCommerce Legacy REST API. * * @since 9.1.0 * * @param string $endpoint Endpoint. * @param array $params Params to pass with request. * @return array|\WP_Error */ public function get_endpoint_data( $endpoint, $params = array() ) { wc_doing_it_wrong( 'get_endpoint_data', "'WC()->api->get_endpoint_data' is deprecated, please use the following instead: wc_get_container()->get(Automattic\WooCommerce\Utilities\RestApiUtil::class)->get_endpoint_data", '9.1.0' ); return $this->rest_api_util->get_endpoint_data( $endpoint, $params ); } } PK �[1]�=k4 k4 Utilities/URL.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; /** * Provides an easy method of assessing URLs, including filepaths (which will be silently * converted to a file:// URL if provided). */ class URL { /** * Components of the URL being assessed. * * The keys match those potentially returned by the parse_url() function, except * that they are always defined and 'drive' (Windows drive letter) has been added. * * @var string|null[] */ private $components = array( 'drive' => null, 'fragment' => null, 'host' => null, 'pass' => null, 'path' => null, 'port' => null, 'query' => null, 'scheme' => null, 'user' => null, ); /** * If the URL (or filepath) is absolute. * * @var bool */ private $is_absolute; /** * If the URL (or filepath) represents a directory other than the root directory. * * This is useful at different points in the process, when deciding whether to re-apply * a trailing slash at the end of processing or when we need to calculate how many * directory traversals are needed to form a (grand-)parent URL. * * @var bool */ private $is_non_root_directory; /** * The components of the URL's path. * * For instance, in the case of "file:///srv/www/wp.site" (noting that a file URL has * no host component) this would contain: * * [ "srv", "www", "wp.site" ] * * In the case of a non-file URL such as "https://example.com/foo/bar/baz" (noting the * host is not part of the path) it would contain: * * [ "foo", "bar", "baz" ] * * @var array */ private $path_parts = array(); /** * The URL. * * @var string */ private $url; /** * Creates and processes the provided URL (or filepath). * * @throws URLException If the URL (or filepath) is seriously malformed. * * @param string $url The URL (or filepath). */ public function __construct( string $url ) { $this->url = $url; $this->preprocess(); $this->process_path(); } /** * Makes all slashes forward slashes, converts filepaths to file:// URLs, and * other processing to help with comprehension of filepaths. * * @throws URLException If the URL is seriously malformed. */ private function preprocess() { // For consistency, all slashes should be forward slashes. $this->url = str_replace( '\\', '/', $this->url ); // Windows: capture the drive letter if provided. if ( preg_match( '#^(file://)?([a-z]):/(?!/).*#i', $this->url, $matches ) ) { $this->components['drive'] = $matches[2]; } /* * If there is no scheme, assume and prepend "file://". An exception is made for cases where the URL simply * starts with exactly two forward slashes, which indicates 'any scheme' (most commonly, that is used when * there is freedom to switch between 'http' and 'https'). */ if ( ! preg_match( '#^[a-z]+://#i', $this->url ) && ! preg_match( '#^//(?!/)#', $this->url ) ) { $this->url = 'file://' . $this->url; } $parsed_components = wp_parse_url( $this->url ); // If we received a really badly formed URL, let's go no further. if ( false === $parsed_components ) { throw new URLException( sprintf( /* translators: %s is the URL. */ __( '%s is not a valid URL.', 'woocommerce' ), $this->url ) ); } $this->components = array_merge( $this->components, $parsed_components ); // File URLs cannot have a host. However, the initial path segment *or* the Windows drive letter // (if present) may be incorrectly be interpreted as the host name. if ( 'file' === $this->components['scheme'] && ! empty( $this->components['host'] ) ) { // If we do not have a drive letter, then simply merge the host and the path together. if ( null === $this->components['drive'] ) { $this->components['path'] = $this->components['host'] . ( $this->components['path'] ?? '' ); } // Restore the host to null in this situation. $this->components['host'] = null; } } /** * Simplifies the path if possible, by resolving directory traversals to the extent possible * without touching the filesystem. */ private function process_path() { $segments = explode( '/', $this->components['path'] ); $this->is_absolute = substr( $this->components['path'], 0, 1 ) === '/' || ! empty( $this->components['host'] ); $this->is_non_root_directory = substr( $this->components['path'], -1, 1 ) === '/' && strlen( $this->components['path'] ) > 1; $resolve_traversals = 'file' !== $this->components['scheme'] || $this->is_absolute; $retain_traversals = false; // Clean the path. foreach ( $segments as $part ) { // Drop empty segments. if ( strlen( $part ) === 0 || '.' === $part ) { continue; } // Directory traversals created with percent-encoding syntax should also be detected. $is_traversal = str_ireplace( '%2e', '.', $part ) === '..'; // Resolve directory traversals (if allowed: see further comment relating to this). if ( $resolve_traversals && $is_traversal ) { if ( count( $this->path_parts ) > 0 && ! $retain_traversals ) { $this->path_parts = array_slice( $this->path_parts, 0, count( $this->path_parts ) - 1 ); continue; } elseif ( $this->is_absolute ) { continue; } } /* * Consider allowing directory traversals to be resolved (ie, the process that converts 'foo/bar/../baz' to * 'foo/baz'). * * 1. For this decision point, we are only concerned with relative filepaths (in all other cases, * $resolve_traversals will already be true). * 2. This is a 'one time' and unidirectional operation. We only wish to flip from false to true, and we * never wish to do this more than once. * 3. We only flip the switch after we have examined all leading '..' traversal segments. */ if ( false === $resolve_traversals && '..' !== $part && 'file' === $this->components['scheme'] && ! $this->is_absolute ) { $resolve_traversals = true; } /* * Set a flag indicating that traversals should be retained. This is done to ensure we don't prematurely * discard traversals at the start of the path. */ $retain_traversals = $resolve_traversals && '..' === $part; // Retain this part of the path. $this->path_parts[] = $part; } // Protect against empty relative paths. if ( count( $this->path_parts ) === 0 && ! $this->is_absolute ) { $this->path_parts = array( '.' ); $this->is_non_root_directory = true; } // Reform the path from the processed segments, appending a leading slash if it is absolute and restoring // the Windows drive letter if we have one. $this->components['path'] = ( $this->is_absolute ? '/' : '' ) . implode( '/', $this->path_parts ) . ( $this->is_non_root_directory ? '/' : '' ); } /** * Returns the processed URL as a string. * * @return string */ public function __toString(): string { return $this->get_url(); } /** * Returns all possible parent URLs for the current URL. * * @return string[] */ public function get_all_parent_urls(): array { $max_parent = count( $this->path_parts ); $parents = array(); /* * If we are looking at a relative path that begins with at least one traversal (example: "../../foo") * then we should only return one parent URL (otherwise, we'd potentially have to return an infinite * number of parent URLs since we can't know how far the tree extends). */ if ( $max_parent > 0 && ! $this->is_absolute && '..' === $this->path_parts[0] ) { $max_parent = 1; } for ( $level = 1; $level <= $max_parent; $level++ ) { $parents[] = $this->get_parent_url( $level ); } return $parents; } /** * Outputs the parent URL. * * For example, if $this->get_url() returns "https://example.com/foo/bar/baz" then * this method will return "https://example.com/foo/bar/". * * When a grand-parent is needed, the optional $level parameter can be used. By default * this is set to 1 (parent). 2 will yield the grand-parent, 3 will yield the great * grand-parent, etc. * * If a level is specified that exceeds the number of path segments, this method will * return false. * * @param int $level Used to indicate the level of parent. * * @return string|false */ public function get_parent_url( int $level = 1 ) { if ( $level < 1 ) { $level = 1; } $parts_count = count( $this->path_parts ); $parent_path_parts_to_keep = $parts_count - $level; /* * With the exception of file URLs, we do not allow obtaining (grand-)parent directories that require * us to describe them using directory traversals. For example, given "http://hostname/foo/bar/baz.png" we do * not permit determining anything more than 2 levels up (we cannot go beyond "http://hostname/"). */ if ( 'file' !== $this->components['scheme'] && $parent_path_parts_to_keep < 0 ) { return false; } // In the specific case of an absolute filepath describing the root directory, there can be no parent. if ( 'file' === $this->components['scheme'] && $this->is_absolute && empty( $this->path_parts ) ) { return false; } // Handle cases where the path starts with one or more 'dot segments'. Since the path has already been // processed, we can be confident that any such segments are at the start of the path. if ( $parts_count > 0 && ( '.' === $this->path_parts[0] || '..' === $this->path_parts[0] ) ) { // Determine the index of the last dot segment (ex: given the path '/../../foo' it would be 1). $single_dots = array_keys( $this->path_parts, '.', true ); $double_dots = array_keys( $this->path_parts, '..', true ); $max_dot_index = max( array_merge( $single_dots, $double_dots ) ); // Prepend the required number of traversals and discard unnecessary trailing segments. $last_traversal = $max_dot_index + ( $this->is_non_root_directory ? 1 : 0 ); $parent_path = str_repeat( '../', $level ) . join( '/', array_slice( $this->path_parts, 0, $last_traversal ) ); } elseif ( $parent_path_parts_to_keep < 0 ) { // For relative filepaths only, we use traversals to describe the requested parent. $parent_path = untrailingslashit( str_repeat( '../', $parent_path_parts_to_keep * -1 ) ); } else { // Otherwise, in a very simple case, we just remove existing parts. $parent_path = implode( '/', array_slice( $this->path_parts, 0, $parent_path_parts_to_keep ) ); } if ( $this->is_relative() && '' === $parent_path ) { $parent_path = '.'; } // Append a trailing slash, since a parent is always a directory. The only exception is the current working directory. $parent_path .= '/'; // For absolute paths, apply a leading slash (does not apply if we have a root path). if ( $this->is_absolute && 0 !== strpos( $parent_path, '/' ) ) { $parent_path = '/' . $parent_path; } // Form the parent URL (ditching the query and fragment, if set). $parent_url = $this->get_url( array( 'path' => $parent_path, 'query' => null, 'fragment' => null, ) ); // We process the parent URL through a fresh instance of this class, for consistency. return ( new self( $parent_url ) )->get_url(); } /** * Outputs the processed URL. * * Borrows from https://www.php.net/manual/en/function.parse-url.php#106731 * * @param array $component_overrides If provided, these will override values set in $this->components. * * @return string */ public function get_url( array $component_overrides = array() ): string { $components = array_merge( $this->components, $component_overrides ); $scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : '//'; $host = null !== $components['host'] ? $components['host'] : ''; $port = null !== $components['port'] ? ':' . $components['port'] : ''; $path = $this->get_path( $components['path'] ); // Special handling for hostless URLs (typically, filepaths) referencing the current working directory. if ( '' === $host && ( '' === $path || '.' === $path ) ) { $path = './'; } $user = null !== $components['user'] ? $components['user'] : ''; $pass = null !== $components['pass'] ? ':' . $components['pass'] : ''; $user_pass = ( ! empty( $user ) || ! empty( $pass ) ) ? $user . $pass . '@' : ''; $query = null !== $components['query'] ? '?' . $components['query'] : ''; $fragment = null !== $components['fragment'] ? '#' . $components['fragment'] : ''; return $scheme . $user_pass . $host . $port . $path . $query . $fragment; } /** * Outputs the path. Especially useful if it was a a regular filepath that was passed in originally. * * @param string|null $path_override If provided this will be used as the URL path. Does not impact drive letter. * * @return string */ public function get_path( ?string $path_override = null ): string { return ( $this->components['drive'] ? $this->components['drive'] . ':' : '' ) . ( $path_override ?? $this->components['path'] ); } /** * Indicates if the URL or filepath was absolute. * * @return bool True if absolute, else false. */ public function is_absolute(): bool { return $this->is_absolute; } /** * Indicates if the URL or filepath was relative. * * @return bool True if relative, else false. */ public function is_relative(): bool { return ! $this->is_absolute; } } PK �[1])aP% P% Utilities/Users.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; use Automattic\WooCommerce\Proxies\LegacyProxy; use WP_Error, WP_User; /** * Helper functions for working with users. */ class Users { /** * Indicates if the user qualifies as site administrator. * * In the context of multisite networks, this means that they must have the `manage_sites` * capability. In all other cases, they must have the `manage_options` capability. * * @param int $user_id Optional, used to specify a specific user (otherwise we look at the current user). * * @return bool */ public static function is_site_administrator( int $user_id = 0 ): bool { $user = 0 === $user_id ? wp_get_current_user() : get_user_by( 'id', $user_id ); if ( false === $user ) { return false; } return is_multisite() ? $user->has_cap( 'manage_sites' ) : $user->has_cap( 'manage_options' ); } /** * Get a user from a valid user ID, but only if the active user is able to see them. * * In a multisite context, that may mean that they both must be members of the current blog, or else the active * user must either have special permissions (manage_network_users) or else a special legacy mode * (woocommerce_network_wide_customers) is enabled. * * @param int $user_id The ID of the desired user. * @param int|null $requesting_user_id The ID of the user making the request. Optional, defaults to the current user. * * @return WP_User|WP_Error */ public static function get_user_in_current_site( $user_id, ?int $requesting_user_id = null ) { // User ID is expected to be an integer. Cast it if we can (avoiding additional runtime warnings), else treat it as 0. $user_id = is_numeric( $user_id ) ? (int) $user_id : 0; $legacy_proxy = wc_get_container()->get( LegacyProxy::class ); $requesting_user_id = $requesting_user_id > 0 ? $requesting_user_id : wp_get_current_user()->ID; $error = new WP_Error( 'wc_user_invalid_id', __( 'Invalid user ID.', 'woocommerce' ) ); if ( $user_id <= 0 ) { return $error; } $user = get_userdata( $user_id ); if ( ! $user instanceof WP_User || ! $user->exists() ) { return $error; } if ( $legacy_proxy->call_function( 'is_multisite' ) && ! $legacy_proxy->call_function( 'is_user_member_of_blog', $user->ID ) && ! $legacy_proxy->call_function( 'user_can', $requesting_user_id, 'manage_network_users' ) && get_site_option( 'woocommerce_network_wide_customers', 'no' ) !== 'yes' ) { return $error; } return $user; } /** * Check if the email is valid. * * @param int $order_id Order ID. * @param string $supplied_email Supplied email. * @param string $context Context in which we are checking the email. * @return bool */ public static function should_user_verify_order_email( $order_id, $supplied_email = null, $context = 'view' ) { $order = wc_get_order( $order_id ); $billing_email = $order->get_billing_email(); $customer_id = $order->get_customer_id(); // If we do not have a billing email for the order (could happen in the order is created manually, or if the // requirement for this has been removed from the checkout flow), email verification does not make sense. if ( empty( $billing_email ) ) { return false; } // No verification step is needed if the user is logged in and is already associated with the order. if ( $customer_id && get_current_user_id() === $customer_id ) { return false; } /** * Controls the grace period within which we do not require any sort of email verification step before rendering * the 'order received' or 'order pay' pages. * * To eliminate the grace period, set to zero (or to a negative value). Note that this filter is not invoked * at all if email verification is deemed to be unnecessary (in other words, it cannot be used to force * verification in *all* cases). * * @since 8.0.0 * * @param int $grace_period Time in seconds after an order is placed before email verification may be required. * @param WC_Order $this The order for which this grace period is being assessed. * @param string $context Indicates the context in which we might verify the email address. Typically 'order-pay' or 'order-received'. */ $verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order, $context ); $date_created = $order->get_date_created(); // We do not need to verify the email address if we are within the grace period immediately following order creation. if ( is_a( $date_created, \WC_DateTime::class, true ) && time() - $date_created->getTimestamp() <= $verification_grace_period ) { return false; } $session = wc()->session; $session_email = ''; if ( is_a( $session, \WC_Session::class ) ) { $customer = $session->get( 'customer' ); $session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : ''; } // Email verification is required if the user cannot be identified, or if they supplied an email address but the nonce check failed. $can_view_orders = current_user_can( 'read_private_shop_orders' ); $session_email_match = $session_email === $billing_email; $supplied_email_match = $supplied_email === $billing_email; $email_verification_required = ! $session_email_match && ! $supplied_email_match && ! $can_view_orders; /** * Provides an opportunity to override the (potential) requirement for shoppers to verify their email address * before we show information such as the order summary, or order payment page. * * Note that this hook is not always triggered, therefore it is (for example) unsuitable as a way of forcing * email verification across all order confirmation/order payment scenarios. Instead, the filter primarily * exists as a way to *remove* the email verification step. * * @since 7.9.0 * * @param bool $email_verification_required If email verification is required. * @param WC_Order $order The relevant order. * @param string $context The context under which we are performing this check. */ return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, $context ); } /** * Site-specific method of retrieving the requested user meta. * * This is a multisite-aware wrapper around WordPress's own `get_user_meta()` function, and works by prefixing the * supplied meta key with a blog-specific meta key. * * @param int $user_id User ID. * @param string $key Optional. The meta key to retrieve. By default, returns data for all keys. * @param bool $single Optional. Whether to return a single value. This parameter has no effect if `$key` is not * specified. Default false. * * @return mixed An array of values if `$single` is false. The value of meta data field if `$single` is true. * False for an invalid `$user_id` (non-numeric, zero, or negative value). An empty string if a valid * but non-existing user ID is passed. */ public static function get_site_user_meta( int $user_id, string $key = '', bool $single = false ) { global $wpdb; $site_specific_key = $key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' ); return get_user_meta( $user_id, $site_specific_key, true ); } /** * Site-specific means of updating user meta. * * This is a multisite-aware wrapper around WordPress's own `update_user_meta()` function, and works by prefixing * the supplied meta key with a blog-specific meta key. * * @param int $user_id User ID. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param mixed $prev_value Optional. Previous value to check before updating. If specified, only update existing * metadata entries with this value. Otherwise, update all entries. Default empty. * * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure or if the value * passed to the function is the same as the one that is already in the database. */ public static function update_site_user_meta( int $user_id, string $meta_key, $meta_value, $prev_value = '' ) { global $wpdb; $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' ); return update_user_meta( $user_id, $site_specific_key, $meta_value, $prev_value ); } /** * Site-specific means of deleting user meta. * * This is a multisite-aware wrapper around WordPress's own `delete_user_meta()` function, and works by prefixing * the supplied meta key with a blog-specific meta key. * * @param int $user_id User ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Optional. Metadata value. If provided, rows will only be removed that match the value. * Must be serializable if non-scalar. Default empty. * * @return bool True on success, false on failure. * / */ public static function delete_site_user_meta( $user_id, $meta_key, $meta_value = '' ) { global $wpdb; $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix(), '_' ); return delete_user_meta( $user_id, $site_specific_key, $meta_value ); } } PK �[1]��� � Utilities/Types.phpnu ��� <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Utilities; use InvalidArgumentException; /** * Utilities to help ensure type safety. */ class Types { /** * Checks if $thing is an instance of $desired_type. * * If the check succeeds, $thing will be returned without further modification. If the check fails, then either * an exception will be thrown or, if an $on_failure callback was supplied, it will be invoked to either generate * an appropriate return value or to throw a more specific exception. * * Please note that the failure handler will be passed two arguments: * * $on_failure( $object, $desired_type ) * * @since 9.1.0 * @throws InvalidArgumentException If $object does not match $desired_type, and an $on_failure callback was not supplied. * * @param mixed $thing The value or reference to be assessed. * @param string $desired_type What we expect the return type to be, if it is not a WP_Error. * @param ?callable $on_failure If provided, and if evaluation fails, this will be invoked to generate a return value. * * @return mixed */ public static function ensure_instance_of( $thing, string $desired_type, ?callable $on_failure = null ) { // If everything looks good, return early. if ( $thing instanceof $desired_type ) { return $thing; } // Summarize the error for use in logging and in case we have to throw an exception. $summary = sprintf( 'Object was not of expected type %1$s.', $desired_type ); // Otherwise, let's log the problem so the site operator has a record of where things went wrong. $logger = wc_get_logger(); if ( $logger ) { $logger->error( $summary, array( 'source' => 'wc-type-check-utility', 'backtrace' => true, ) ); } // Invoke the $on_failure handler, if specified. if ( null !== $on_failure ) { return $on_failure( $thing, $desired_type ); } throw new InvalidArgumentException( esc_html( $summary ) ); } } PK �[1]�kc@ c@ Utilities/DatabaseUtil.phpnu ��� <?php /** * DatabaseUtil class file. */ namespace Automattic\WooCommerce\Internal\Utilities; use DateTime; use DateTimeZone; use Vtiful\Kernel\Format; /** * A class of utilities for dealing with the database. */ class DatabaseUtil { /** * Wrapper for the WordPress dbDelta function, allows to execute a series of SQL queries. * * @param string $queries The SQL queries to execute. * @param bool $execute Ture to actually execute the queries, false to only simulate the execution. * @return array The result of the execution (or simulation) from dbDelta. */ public function dbdelta( string $queries = '', bool $execute = true ): array { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; return dbDelta( $queries, $execute ); } /** * Given a set of table creation SQL statements, check which of the tables are currently missing in the database. * * @param string $creation_queries The SQL queries to execute ("CREATE TABLE" statements, same format as for dbDelta). * @return array An array containing the names of the tables that currently don't exist in the database. */ public function get_missing_tables( string $creation_queries ): array { global $wpdb; $suppress_errors = $wpdb->suppress_errors( true ); $dbdelta_output = $this->dbdelta( $creation_queries, false ); $wpdb->suppress_errors( $suppress_errors ); $parsed_output = $this->parse_dbdelta_output( $dbdelta_output ); return $parsed_output['created_tables']; } /** * Parses the output given by dbdelta and returns information about it. * * @param array $dbdelta_output The output from the execution of dbdelta. * @return array[] An array containing a 'created_tables' key whose value is an array with the names of the tables that have been (or would have been) created. */ public function parse_dbdelta_output( array $dbdelta_output ): array { $created_tables = array(); foreach ( $dbdelta_output as $table_name => $result ) { if ( "Created table $table_name" === $result ) { $created_tables[] = str_replace( '(', '', $table_name ); } } return array( 'created_tables' => $created_tables ); } /** * Drops a database table. * * @param string $table_name The name of the table to drop. * @param bool $add_prefix True if the table name passed needs to be prefixed with $wpdb->prefix before processing. * @return bool True on success, false on error. */ public function drop_database_table( string $table_name, bool $add_prefix = false ) { global $wpdb; if ( $add_prefix ) { $table_name = $wpdb->prefix . $table_name; } //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->query( "DROP TABLE IF EXISTS `{$table_name}`" ); } /** * Drops a table index, if both the table and the index exist. * * @param string $table_name The name of the table that contains the index. * @param string $index_name The name of the index to be dropped. * @return bool True if the index has been dropped, false if either the table or the index don't exist. */ public function drop_table_index( string $table_name, string $index_name ): bool { global $wpdb; if ( empty( $this->get_index_columns( $table_name, $index_name ) ) ) { return false; } // phpcs:ignore WordPress.DB.PreparedSQL $wpdb->query( "ALTER TABLE $table_name DROP INDEX $index_name" ); return true; } /** * Create a primary key for a table, only if the table doesn't have a primary key already. * * @param string $table_name Table name. * @param array $columns An array with the index column names. * @return bool True if the key has been created, false if the table already had a primary key. */ public function create_primary_key( string $table_name, array $columns ) { global $wpdb; if ( ! empty( $this->get_index_columns( $table_name ) ) ) { return false; } // phpcs:ignore WordPress.DB.PreparedSQL $wpdb->query( "ALTER TABLE $table_name ADD PRIMARY KEY(`" . join( '`,`', $columns ) . '`)' ); return true; } /** * Get the columns of a given table index, or of the primary key. * * @param string $table_name Table name. * @param string $index_name Index name, empty string for the primary key. * @return array The index columns. Empty array if the table or the index don't exist. */ public function get_index_columns( string $table_name, string $index_name = '' ): array { global $wpdb; if ( empty( $index_name ) ) { $index_name = 'PRIMARY'; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $wpdb->prepare( "SHOW INDEX FROM $table_name WHERE Key_name = %s", $index_name ) ); if ( empty( $results ) ) { return array(); } return array_column( $results, 'Column_name' ); } /** * Formats an object value of type `$type` for inclusion in the database. * * @param mixed $value Raw value. * @param string $type Data type. * @return mixed * @throws \Exception When an invalid type is passed. */ public function format_object_value_for_db( $value, string $type ) { switch ( $type ) { case 'decimal': $value = wc_format_decimal( $value, false, true ); break; case 'int': $value = (int) $value; break; case 'bool': $value = wc_string_to_bool( $value ); break; case 'string': $value = strval( $value ); break; case 'date': // Date properties are converted to the WP timezone (see WC_Data::set_date_prop() method), however // for our own tables we persist dates in GMT. $value = $value ? ( new DateTime( $value ) )->setTimezone( new DateTimeZone( '+00:00' ) )->format( 'Y-m-d H:i:s' ) : null; break; case 'date_epoch': $value = $value ? ( new DateTime( "@{$value}" ) )->format( 'Y-m-d H:i:s' ) : null; break; default: throw new \Exception( esc_html( 'Invalid type received: ' . $type ) ); } return $value; } /** * Returns the `$wpdb` placeholder to use for data type `$type`. * * @param string $type Data type. * @return string * @throws \Exception When an invalid type is passed. */ public function get_wpdb_format_for_type( string $type ) { static $wpdb_placeholder_for_type = array( 'int' => '%d', 'decimal' => '%f', 'string' => '%s', 'date' => '%s', 'date_epoch' => '%s', 'bool' => '%d', ); if ( ! isset( $wpdb_placeholder_for_type[ $type ] ) ) { throw new \Exception( esc_html( 'Invalid column type: ' . $type ) ); } return $wpdb_placeholder_for_type[ $type ]; } /** * Generates ON DUPLICATE KEY UPDATE clause to be used in migration. * * @param array $columns List of column names. * * @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE */ public function generate_on_duplicate_statement_clause( array $columns ): string { $update_value_statements = array(); foreach ( $columns as $column ) { $update_value_statements[] = "`$column` = VALUES( `$column` )"; } $update_value_clause = implode( ', ', $update_value_statements ); return "ON DUPLICATE KEY UPDATE $update_value_clause"; } /** * Hybrid of $wpdb->update and $wpdb->insert. It will try to update a row, and if it doesn't exist, it will insert it. This needs unique constraints to be set on the table on all ID columns. * * You can use this function only when: * 1. There is only one unique constraint on the table. The constraint can contain multiple columns, but it must be the only one unique constraint. * 2. The complete unique constraint must be part of the $data array. * 3. You do not need the LAST_INSERT_ID() value. * * @param string $table_name Table name. * @param array $data Unescaped data to update (in column => value pairs). * @param array $format An array of formats to be mapped to each of the values in $data. * * @return int Returns the value of DB's ON DUPLICATE KEY UPDATE clause. */ public function insert_on_duplicate_key_update( $table_name, $data, $format ): int { global $wpdb; if ( empty( $data ) ) { return 0; } $columns = array_keys( $data ); $value_format = array(); $values = array(); $index = 0; // Directly use NULL for placeholder if the value is NULL, since otherwise $wpdb->prepare will convert it to empty string. foreach ( $data as $key => $value ) { if ( is_null( $value ) ) { $value_format[] = 'NULL'; } else { $values[] = $value; $value_format[] = $format[ $index ]; } ++$index; } $column_clause = '`' . implode( '`, `', $columns ) . '`'; $value_format_clause = implode( ', ', $value_format ); $on_duplicate_clause = $this->generate_on_duplicate_statement_clause( $columns ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Values are escaped in $wpdb->prepare. $sql = $wpdb->prepare( " INSERT INTO $table_name ( $column_clause ) VALUES ( $value_format_clause ) $on_duplicate_clause ", $values ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is prepared. return $wpdb->query( $sql ); } /** * Hybrid of $wpdb->update and $wpdb->insert. It will try to update a row, and if it doesn't exist, it will insert it. Unlike `insert_on_duplicate_key_update` it does not require a unique constraint, but also does not guarantee uniqueness on its own. * * When a unique constraint is present, it will perform better than the `insert_on_duplicate_key_update` since it needs fewer locks. * * Note that it will only update at max just 1 database row, unlike `wpdb->update` which updates everything that matches the `$where` criteria. This is also why it needs a primary_key_column. * * @param string $table_name Table Name. * @param array $data Data to insert update in array($column_name => $value) format. * @param array $where Update conditions in array($column_name => $value) format. Conditions will be joined by AND. * @param array $format Format strings for data. Unlike $wpdb->update/insert, this method won't guess the format, and has to be provided explicitly. * @param array $where_format Format strings for where conditions. Unlike $wpdb->update/insert, this method won't guess the format, and has to be provided explicitly. * @param string $primary_key_column Name of the Primary key column. * @param string $primary_key_format Format for primary key. * * @return bool|int Number of rows affected. Boolean false on error. */ public function insert_or_update( $table_name, $data, $where, $format, $where_format, $primary_key_column = 'id', $primary_key_format = '%d' ) { global $wpdb; if ( empty( $data ) || empty( $where ) ) { return 0; } // Build select query. $values = array(); $index = 0; $conditions = array(); foreach ( $where as $column => $value ) { if ( is_null( $value ) ) { $conditions[] = "`$column` IS NULL"; continue; } $conditions[] = "`$column` = " . $where_format[ $index ]; $values[] = $value; ++$index; } $conditions = implode( ' AND ', $conditions ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $primary_key_column and $table_name are hardcoded. $conditions is being prepared. $query = $wpdb->prepare( "SELECT `$primary_key_column` FROM `$table_name` WHERE $conditions LIMIT 1", $values ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above. $row_id = $wpdb->get_var( $query ); if ( $row_id ) { // Update the row. $result = $wpdb->update( $table_name, $data, array( $primary_key_column => $row_id ), $format, array( $primary_key_format ) ); } else { // Insert the row. $result = $wpdb->insert( $table_name, $data, $format ); } return $result; } /** * Get max index length. * * @return int Max index length. */ public function get_max_index_length(): int { /** * Filters the maximum index length in the database. * * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. * * Additionally, MyISAM engine also limits the index size to 1000 bytes. We add this filter so that interested folks on InnoDB engine can increase the size till allowed 3071 bytes. * * @param int $max_index_length Maximum index length. Default 191. * * @since 8.0.0 */ $max_index_length = apply_filters( 'woocommerce_database_max_index_length', 191 ); // Index length cannot be more than 768, which is 3078 bytes in utf8mb4 and max allowed by InnoDB engine. return min( absint( $max_index_length ), 767 ); } /** * Create a fulltext index on order address table. * * @return void */ public function create_fts_index_order_address_table(): void { global $wpdb; $address_table = $wpdb->prefix . 'wc_order_addresses'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $address_table is hardcoded. $wpdb->query( "CREATE FULLTEXT INDEX order_addresses_fts ON $address_table (first_name, last_name, company, address_1, address_2, city, state, postcode, country, email, phone)" ); } /** * Helper method to drop the fulltext index on order address table. * * @since 9.4.0 * * @return void */ public function drop_fts_index_order_address_table(): void { global $wpdb; $address_table = $wpdb->prefix . 'wc_order_addresses'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $address_table is hardcoded. $wpdb->query( "ALTER TABLE $address_table DROP INDEX order_addresses_fts;" ); } /** * Sanitize FTS Search params to remove relevancy operators for performance, and add partial matches. Useful when the sorting is already happening based on some other conditions, so relevancy calculation is not needed. * * @since 9.4.0 * * @param string $param Search term. * * @return string Sanitized search term. */ public function sanitise_boolean_fts_search_term( string $param ): string { // Remove any operator to prevent incorrect query and fatals, such as search starting with `++`. We can allow this in the future if we have proper validation for FTS search operators. // Space is allowed to provide multiple words. $sanitized_param = preg_replace( '/[^\p{L}\p{N}_]+/u', ' ', $param ); if ( $sanitized_param !== $param ) { $param = str_replace( '"', '', $param ); return '"' . $param . '"'; } // Split the search phrase into words so that we can add operators when needed. $words = explode( ' ', $param ); $sanitized_words = array(); foreach ( $words as $word ) { // Add `*` as suffix to every term so that partial matches happens. $word = $word . '*'; $sanitized_words[] = $word; } return implode( ' ', $sanitized_words ); } /** * Check if fulltext index with key `order_addresses_fts` on order address table exists. * * @return bool */ public function fts_index_on_order_address_table_exists(): bool { global $wpdb; $address_table = $wpdb->prefix . 'wc_order_addresses'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $address_table is hardcoded. return ! empty( $wpdb->get_results( "SHOW INDEX FROM $address_table WHERE Key_name = 'order_addresses_fts'" ) ); } /** * Create a fulltext index on order item table. * * @return void */ public function create_fts_index_order_item_table(): void { global $wpdb; $order_item_table = $wpdb->prefix . 'woocommerce_order_items'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_item_table is hardcoded. $wpdb->query( "CREATE FULLTEXT INDEX order_item_fts ON $order_item_table (order_item_name)" ); } /** * Check if fulltext index with key `order_item_fts` on order item table exists. * * @return bool */ public function fts_index_on_order_item_table_exists(): bool { global $wpdb; $order_item_table = $wpdb->prefix . 'woocommerce_order_items'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_item_table is hardcoded. return ! empty( $wpdb->get_results( "SHOW INDEX FROM $order_item_table WHERE Key_name = 'order_item_fts'" ) ); } } PK �[1]��ަC C Utilities/ArrayUtil.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\Utilities; /** * A class of utilities for dealing with arrays. */ class ArrayUtil { /** * Determines if the given array is a list. * * An array is considered a list if its keys consist of consecutive numbers from 0 to count($array)-1. * * Polyfill for array_is_list() in PHP 8.1. * * @param array $arr The array being evaluated. * * @return bool True if array is a list, false otherwise. */ public static function array_is_list( array $arr ): bool { if ( function_exists( 'array_is_list' ) ) { return array_is_list( $arr ); } if ( ( array() === $arr ) || ( array_values( $arr ) === $arr ) ) { return true; } $next_key = -1; foreach ( $arr as $k => $v ) { if ( ++$next_key !== $k ) { return false; } } return true; } /** * Merge two lists of associative arrays by a key. * * @param array $arr1 The first array. * @param array $arr2 The second array. * @param string $key The key to merge by. * * @return array The merged list sorted by the key values. */ public static function merge_by_key( array $arr1, array $arr2, string $key ): array { $merged = array(); // Overwrite items in $arr1 with items in $arr2 if they have the same key entry value. // The rest of items in $arr1 will be appended. foreach ( $arr1 as $item1 ) { $found = false; foreach ( $arr2 as $item2 ) { if ( $item1[ $key ] === $item2[ $key ] ) { $merged[] = array_merge( $item1, $item2 ); $found = true; break; } } if ( ! $found ) { $merged[] = $item1; } } // Append items from $arr2 that are don't have a corresponding key entry value in $arr1. foreach ( $arr2 as $item2 ) { $found = false; foreach ( $arr1 as $item1 ) { if ( $item1[ $key ] === $item2[ $key ] ) { $found = true; break; } } if ( ! $found ) { $merged[] = $item2; } } // Sort the merged list by the key values. usort( $merged, function ( $a, $b ) use ( $key ) { return $a[ $key ] <=> $b[ $key ]; } ); return array_values( $merged ); } /** * Recursively filters null values from an array. * * This method removes all null values from the array, including nested arrays. * Array keys are preserved for associative arrays. For lists (sequential numeric * keys starting from 0), the array is reindexed to maintain the list structure. * * @param array $arr The array to filter. * * @return array The filtered array with null values removed. */ public static function filter_null_values_recursive( array $arr ): array { $is_list = self::array_is_list( $arr ); $filtered = array(); foreach ( $arr as $key => $value ) { // Skip null values. if ( is_null( $value ) ) { continue; } // Recursively filter nested arrays. if ( is_array( $value ) ) { $filtered[ $key ] = self::filter_null_values_recursive( $value ); } else { $filtered[ $key ] = $value; } } // Reindex if the original array was a list. return $is_list ? array_values( $filtered ) : $filtered; } } PK �[1]W"��o o Utilities/BlocksUtil.phpnu ��� <?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\Utilities; /** * Helper functions for working with blocks. */ class BlocksUtil { /** * Return blocks with their inner blocks flattened. * * @param array $blocks Array of blocks as returned by parse_blocks(). * @return array All blocks. */ public static function flatten_blocks( $blocks ) { return array_reduce( $blocks, function ( $carry, $block ) { array_push( $carry, array_diff_key( $block, array_flip( array( 'innerBlocks' ) ) ) ); if ( isset( $block['innerBlocks'] ) ) { $inner_blocks = self::flatten_blocks( $block['innerBlocks'] ); return array_merge( $carry, $inner_blocks ); } return $carry; }, array() ); } /** * Get all instances of the specified block from the widget area. * * @param string $block_name The name (id) of a block, e.g. `woocommerce/mini-cart`. * @return array Array of blocks as returned by parse_blocks(). */ public static function get_blocks_from_widget_area( $block_name ) { $blocks = get_option( 'widget_block' ); if ( ! is_array( $blocks ) || empty( $blocks ) ) { return array(); } return array_reduce( $blocks, function ( $acc, $block ) use ( $block_name ) { $parsed_blocks = ! empty( $block['content'] ) ? parse_blocks( $block['content'] ) : array(); if ( ! empty( $parsed_blocks ) && $block_name === $parsed_blocks[0]['blockName'] ) { array_push( $acc, $parsed_blocks[0] ); } return $acc; }, array() ); } /** * Get all instances of the specified block on a specific template part. * * @param string $block_name The name (id) of a block, e.g. `woocommerce/mini-cart`. * @param string $template_part_slug The woo page to search, e.g. `header`. * @return array Array of blocks as returned by parse_blocks(). */ public static function get_block_from_template_part( $block_name, $template_part_slug ) { $template = get_block_template( get_stylesheet() . '//' . $template_part_slug, 'wp_template_part' ); if ( ! $template || null === $template->content ) { return array(); } $blocks = parse_blocks( $template->content ); $flatten_blocks = self::flatten_blocks( $blocks ); return array_values( array_filter( $flatten_blocks, function ( $block ) use ( $block_name ) { return ( $block_name === $block['blockName'] ); } ) ); } } PK �[1]����c c Utilities/HtmlSanitizer.phpnu ��� <?php namespace Automattic\WooCommerce\Internal\Utilities; /** * Utility for re-using WP Kses-based sanitization rules. */ class HtmlSanitizer { /** * Rules for allowing minimal HTML (breaks, images, paragraphs and spans) without any links. */ public const LOW_HTML_BALANCED_TAGS_NO_LINKS = array( 'pre_processors' => array( 'stripslashes', 'force_balance_tags', ), 'wp_kses_rules' => array( 'br' => true, 'img' => array( 'alt' => true, 'class' => true, 'src' => true, 'title' => true, ), 'p' => array( 'class' => true, ), 'span' => array( 'class' => true, 'title' => true, ), ), ); /** * Sanitizes a chunk of HTML, by following the same rules as `wp_kses_post()` but also allowing * the style element to be supplied. * * @param string $html The HTML to be sanitized. * * @return string */ public function styled_post_content( string $html ): string { $rules = wp_kses_allowed_html( 'post' ); $rules['style'] = true; return wp_kses( $html, $rules ); } /** * Sanitizes the HTML according to the provided rules. * * @see wp_kses() * * @param string $html HTML string to be sanitized. * @param array $sanitizer_rules { * Optional and defaults to self::TRIMMED_BALANCED_LOW_HTML_NO_LINKS. Otherwise, one or more of the following * keys should be set. * * @type array $pre_processors Callbacks to run before invoking `wp_kses()`. * @type array $wp_kses_rules Element names and attributes to allow, per `wp_kses()`. * } * * @return string */ public function sanitize( string $html, array $sanitizer_rules = self::LOW_HTML_BALANCED_TAGS_NO_LINKS ): string { if ( isset( $sanitizer_rules['pre_processors'] ) && is_array( $sanitizer_rules['pre_processors'] ) ) { $html = $this->apply_string_callbacks( $sanitizer_rules['pre_processors'], $html ); } // If no KSES rules are specified, assume all HTML should be stripped. $kses_rules = isset( $sanitizer_rules['wp_kses_rules'] ) && is_array( $sanitizer_rules['wp_kses_rules'] ) ? $sanitizer_rules['wp_kses_rules'] : array(); return wp_kses( $html, $kses_rules ); } /** * Applies callbacks used to process the string before and after wp_kses(). * * If a callback is invalid we will short-circuit and return an empty string, on the grounds that it is better to * output nothing than risky HTML. We also call the problem out via _doing_it_wrong() to highlight the problem (and * increase the chances of this being caught during development). * * @param callable[] $callbacks The callbacks used to mutate the string. * @param string $string The string being processed. * * @return string */ private function apply_string_callbacks( array $callbacks, string $string ): string { foreach ( $callbacks as $callback ) { if ( ! is_callable( $callback ) ) { _doing_it_wrong( __CLASS__ . '::apply', esc_html__( 'String processors must be an array of valid callbacks.', 'woocommerce' ), esc_html( WC()->version ) ); return ''; } $string = (string) $callback( $string ); } return $string; } } PK �[1]��^�& & Utilities/COTMigrationUtil.phpnu ��� <?php /** * Utility functions meant for helping in migration from posts tables to custom order tables. */ namespace Automattic\WooCommerce\Internal\Utilities; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\{ DataSynchronizer, OrdersTableDataStore }; use WC_Order; use WP_Post; /** * Utility functions meant for helping in migration from posts tables to custom order tables. */ class COTMigrationUtil { /** * Custom order table controller. * * @var CustomOrdersTableController */ private $table_controller; /** * Data synchronizer. * * @var DataSynchronizer */ private $data_synchronizer; /** * Initialize method, invoked by the DI container. * * @internal Automatically called by the container. * @param CustomOrdersTableController $table_controller Custom order table controller. * @param DataSynchronizer $data_synchronizer Data synchronizer. * * @return void */ final public function init( CustomOrdersTableController $table_controller, DataSynchronizer $data_synchronizer ) { $this->table_controller = $table_controller; $this->data_synchronizer = $data_synchronizer; } /** * Helper function to get screen name of orders page in wp-admin. * * @throws \Exception If called from outside of wp-admin. * * @return string */ public function get_order_admin_screen() : string { if ( ! is_admin() ) { throw new \Exception( 'This function should only be called in admin.' ); } return $this->custom_orders_table_usage_is_enabled() && function_exists( 'wc_get_page_screen_id' ) ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order'; } /** * Helper function to get whether custom order tables are enabled or not. * * @return bool */ private function custom_orders_table_usage_is_enabled() : bool { return $this->table_controller->custom_orders_table_usage_is_enabled(); } /** * Checks if posts and order custom table sync is enabled and there are no pending orders. * * @return bool */ public function is_custom_order_tables_in_sync() : bool { if ( ! $this->data_synchronizer->data_sync_is_enabled() ) { return false; } return ! $this->data_synchronizer->has_orders_pending_sync(); } /** * Gets value of a meta key from WC_Data object if passed, otherwise from the post object. * This helper function support backward compatibility for meta box functions, when moving from posts based store to custom tables. * * @param WP_Post|null $post Post object, meta will be fetched from this only when `$data` is not passed. * @param \WC_Data|null $data WC_Data object, will be preferred over post object when passed. * @param string $key Key to fetch metadata for. * @param bool $single Whether metadata is single. * * @return array|mixed|string Value of the meta key. */ public function get_post_or_object_meta( ?WP_Post $post, ?\WC_Data $data, string $key, bool $single ) { if ( isset( $data ) ) { if ( method_exists( $data, "get$key" ) ) { return $data->{"get$key"}(); } return $data->get_meta( $key, $single ); } else { return isset( $post->ID ) ? get_post_meta( $post->ID, $key, $single ) : false; } } /** * Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering. * * @param WC_Order|WP_Post $post_or_order_object Post or order object. * * @return bool|WC_Order|WC_Order_Refund WC_Order object. */ public function init_theorder_object( $post_or_order_object ) { global $theorder; if ( $theorder instanceof WC_Order ) { return $theorder; } if ( $post_or_order_object instanceof WC_Order ) { $theorder = $post_or_order_object; } else { $theorder = wc_get_order( $post_or_order_object->ID ); } return $theorder; } /** * Helper function to get ID from a post or order object. * * @param WP_Post/WC_Order $post_or_order_object WP_Post/WC_Order object to get ID for. * * @return int Order or post ID. */ public function get_post_or_order_id( $post_or_order_object ) : int { if ( is_numeric( $post_or_order_object ) ) { return (int) $post_or_order_object; } elseif ( $post_or_order_object instanceof WC_Order ) { return $post_or_order_object->get_id(); } elseif ( $post_or_order_object instanceof WP_Post ) { return $post_or_order_object->ID; } return 0; } /** * Checks if passed id, post or order object is a WC_Order object. * * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object. * @param string[] $types Types to match against. * * @return bool Whether the passed param is an order. */ public function is_order( $order_id, array $types = array( 'shop_order' ) ) : bool { $order_id = $this->get_post_or_order_id( $order_id ); $order_data_store = \WC_Data_Store::load( 'order' ); return in_array( $order_data_store->get_order_type( $order_id ), $types, true ); } /** * Returns type pf passed id, post or order object. * * @param int|WP_Post|WC_Order $order_id Order ID, post object or order object. * * @return string|null Type of the order. */ public function get_order_type( $order_id ) { $order_id = $this->get_post_or_order_id( $order_id ); $order_data_store = \WC_Data_Store::load( 'order' ); return $order_data_store->get_order_type( $order_id ); } /** * Get the name of the database table that's currently in use for orders. * * @return string */ public function get_table_for_orders() { if ( $this->custom_orders_table_usage_is_enabled() ) { $table_name = OrdersTableDataStore::get_orders_table_name(); } else { global $wpdb; $table_name = $wpdb->posts; } return $table_name; } /** * Get the name of the database table that's currently in use for orders. * * @return string */ public function get_table_for_order_meta() { if ( $this->custom_orders_table_usage_is_enabled() ) { $table_name = OrdersTableDataStore::get_meta_table_name(); } else { global $wpdb; $table_name = $wpdb->postmeta; } return $table_name; } } PK �[1]ZD�� � Utilities/ProductUtil.phpnu ��� <?php declare(strict_types=1); namespace Automattic\WooCommerce\Internal\Utilities; /** * Class with general utility methods related to products. */ class ProductUtil { /** * Delete the transients related to a specific product. * If the product is a variation, delete the transients for the parent too. * * @param WC_Product|int $product_or_id The product or the product id. * @return void */ public function delete_product_specific_transients( $product_or_id ) { $parent_id = 0; if ( $product_or_id instanceof \WC_Product ) { $product = $product_or_id; $product_id = $product->get_id(); } else { $product_id = $product_or_id; $product = wc_get_product( $product_id ); } if ( $product instanceof \WC_Product_Variation ) { $parent_id = $product->get_parent_id(); } $product_specific_transient_names = array( 'wc_product_children_', 'wc_var_prices_', 'wc_related_', 'wc_child_has_weight_', 'wc_child_has_dimensions_', ); foreach ( $product_specific_transient_names as $transient ) { delete_transient( $transient . $product_id ); if ( $parent_id ) { delete_transient( $transient . $parent_id ); } } } } PK �[1]�Й�y y Utilities/WebhookUtil.phpnu ��� <?php /** * WebhookUtil class file. */ namespace Automattic\WooCommerce\Internal\Utilities; use WC_Cache_Helper; /** * Class with utility methods for dealing with webhooks. */ class WebhookUtil { /** * Creates a new instance of the class. */ public function __construct() { add_action( 'deleted_user', array( $this, 'reassign_webhooks_to_new_user_id' ), 10, 2 ); add_action( 'delete_user_form', array( $this, 'maybe_render_user_with_webhooks_warning' ), 10, 2 ); } /** * Whenever a user is deleted, re-assign their webhooks to the new user. * * If re-assignment isn't selected during deletion, assign the webhooks to user_id 0, * so that an admin can edit and re-save them in order to get them to be assigned to a valid user. * * @param int $old_user_id ID of the deleted user. * @param int|null $new_user_id ID of the user to reassign existing data to, or null if no re-assignment is requested. * * @return void * @since 7.8.0 * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function reassign_webhooks_to_new_user_id( int $old_user_id, ?int $new_user_id ): void { $webhook_ids = $this->get_webhook_ids_for_user( $old_user_id ); foreach ( $webhook_ids as $webhook_id ) { $webhook = new \WC_Webhook( $webhook_id ); $webhook->set_user_id( $new_user_id ?? 0 ); $webhook->save(); } } /** * When users are about to be deleted show an informative text if they have webhooks assigned. * * @param \WP_User $current_user The current logged in user. * @param array $userids Array with the ids of the users that are about to be deleted. * @return void * @since 7.8.0 * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function maybe_render_user_with_webhooks_warning( \WP_User $current_user, array $userids ): void { global $wpdb; $at_least_one_user_with_webhooks = false; foreach ( $userids as $user_id ) { $webhook_ids = $this->get_webhook_ids_for_user( $user_id ); if ( empty( $webhook_ids ) ) { continue; } $at_least_one_user_with_webhooks = true; $user_data = get_userdata( $user_id ); $user_login = false === $user_data ? '' : $user_data->user_login; $webhooks_count = count( $webhook_ids ); $text = sprintf( /* translators: 1 = user id, 2 = user login, 3 = webhooks count */ _nx( 'User #%1$s %2$s has created %3$d WooCommerce webhook.', 'User #%1$s %2$s has created %3$d WooCommerce webhooks.', $webhooks_count, 'user webhook count', 'woocommerce' ), $user_id, $user_login, $webhooks_count ); echo '<p>' . esc_html( $text ) . '</p>'; } if ( ! $at_least_one_user_with_webhooks ) { return; } $webhooks_settings_url = esc_url_raw( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) ); // This block of code is copied from WordPress' users.php. // phpcs:disable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared $users_have_content = (bool) apply_filters( 'users_have_additional_content', false, $userids ); if ( ! $users_have_content ) { if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) { $users_have_content = true; } elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) { $users_have_content = true; } } // phpcs:enable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared if ( $users_have_content ) { $text = __( 'If the "Delete all content" option is selected, the affected WooCommerce webhooks will <b>not</b> be deleted and will be attributed to user id 0.<br/>', 'woocommerce' ); } else { $text = __( 'The affected WooCommerce webhooks will <b>not</b> be deleted and will be attributed to user id 0.<br/>', 'woocommerce' ); } $text .= sprintf( /* translators: 1 = url of the WooCommerce webhooks settings page */ __( 'After that they can be reassigned to the logged-in user by going to the <a href="%1$s">WooCommerce webhooks settings page</a> and re-saving them.', 'woocommerce' ), $webhooks_settings_url ); echo '<p>' . wp_kses_post( $text ) . '</p>'; } /** * Get the ids of the webhooks assigned to a given user. * * @param int $user_id User id. * @return int[] Array of webhook ids. */ private function get_webhook_ids_for_user( int $user_id ): array { $data_store = \WC_Data_Store::load( 'webhook' ); return $data_store->search_webhooks( array( 'user_id' => $user_id, ) ); } /** * Gets the count of webhooks that are configured to use the Legacy REST API to compose their payloads. * * @param bool $clear_cache If true, the previously cached value of the count will be discarded if it exists. * * @return int */ public function get_legacy_webhooks_count( bool $clear_cache = false ): int { global $wpdb; $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'legacy_count'; if ( $clear_cache ) { wp_cache_delete( $cache_key, 'webhooks' ); } $count = wp_cache_get( $cache_key, 'webhooks' ); if ( false === $count ) { $count = absint( $wpdb->get_var( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `api_version` < 1;" ) ); wp_cache_add( $cache_key, $count, 'webhooks' ); } return $count; } } PK �[1]��u&P! P! OrderCouponDataMigrator.phpnu ��� <?php namespace Automattic\WooCommerce\Internal; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface; use Automattic\WooCommerce\Utilities\StringUtil; use \Exception; /** * This class is intended to be used with BatchProcessingController and converts verbose * 'coupon_data' metadata entries in coupon line items (corresponding to coupons applied to orders) * into simplified 'coupon_info' entries. See WC_Coupon::get_short_info. * * Additionally, this class manages the "Convert order coupon data" tool. */ class OrderCouponDataMigrator implements BatchProcessorInterface, RegisterHooksInterface { /** * Register hooks for the class. */ public function register() { add_filter( 'woocommerce_debug_tools', array( $this, 'handle_woocommerce_debug_tools' ), 999, 1 ); } /** * Get a user-friendly name for this processor. * * @return string Name of the processor. */ public function get_name(): string { return "Coupon line item 'coupon_data' to 'coupon_info' metadata migrator"; } /** * Get a user-friendly description for this processor. * * @return string Description of what this processor does. */ public function get_description(): string { return "Migrates verbose metadata about coupons applied to an order ('coupon_data' metadata key in coupon line items) to simplified metadata ('coupon_info' keys)"; } /** * Get the total number of pending items that require processing. * * @return int Number of items pending processing. */ public function get_total_pending_count(): int { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key=%s", 'coupon_data' ) ); } /** * Returns the next batch of items that need to be processed. * A batch in this context is a list of 'meta_id' values from the wp_woocommerce_order_itemmeta table. * * @param int $size Maximum size of the batch to be returned. * * @return array Batch of items to process, containing $size or less items. */ public function get_next_batch_to_process( int $size ): array { global $wpdb; $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key=%s ORDER BY meta_id ASC LIMIT %d", 'coupon_data', $size ) ); return array_map( 'absint', $meta_ids ); } /** * Process data for the supplied batch. See the convert_item method. * * @throw \Exception Something went wrong while processing the batch. * * @param array $batch Batch to process, as returned by 'get_next_batch_to_process'. */ public function process_batch( array $batch ): void { global $wpdb; if ( empty( $batch ) ) { return; } $meta_ids = StringUtil::to_sql_list( $batch ); $meta_ids_and_values = $wpdb->get_results( //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT meta_id,meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_id IN $meta_ids", ARRAY_N ); foreach ( $meta_ids_and_values as $meta_id_and_value ) { try { $this->convert_item( (int) $meta_id_and_value[0], $meta_id_and_value[1] ); } catch ( Exception $ex ) { wc_get_logger()->error( StringUtil::class_name_without_namespace( self::class ) . ": when converting meta row with id {$meta_id_and_value[0]}: {$ex->getMessage()}" ); } } } /** * Convert one verbose 'coupon_data' entry into a simplified 'coupon_info' entry. * * The existing database row is updated in place, both the 'meta_key' and the 'meta_value' columns. * * @param int $meta_id Value of 'meta_id' of the row being converted. * @param string $meta_value Value of 'meta_value' of the row being converted. * @throws Exception Database error. */ private function convert_item( int $meta_id, string $meta_value ) { global $wpdb; //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize $coupon_data = unserialize( $meta_value ); $temp_coupon = new \WC_Coupon(); $temp_coupon->set_props( $coupon_data ); //phpcs:disable WordPress.DB.SlowDBQuery $wpdb->update( "{$wpdb->prefix}woocommerce_order_itemmeta", array( 'meta_key' => 'coupon_info', 'meta_value' => $temp_coupon->get_short_info(), ), array( 'meta_id' => $meta_id ) ); //phpcs:enable WordPress.DB.SlowDBQuery if ( $wpdb->last_error ) { throw new Exception( $wpdb->last_error ); } } /** * Default (preferred) batch size to pass to 'get_next_batch_to_process'. * * @return int Default batch size. */ public function get_default_batch_size(): int { return 1000; } /** * Add the tool to start or stop the background process that converts order coupon metadata entries. * * @param array $tools Old tools array. * @return array Updated tools array. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function handle_woocommerce_debug_tools( array $tools ): array { $batch_processor = wc_get_container()->get( BatchProcessingController::class ); $pending_count = $this->get_total_pending_count(); if ( 0 === $pending_count ) { $tools['start_convert_order_coupon_data'] = array( 'name' => __( 'Start converting order coupon data to the simplified format', 'woocommerce' ), 'button' => __( 'Start converting', 'woocommerce' ), 'disabled' => true, 'desc' => __( 'This will convert <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. The conversion will happen overtime in the background (via Action Scheduler). There are currently no entries to convert.', 'woocommerce' ), ); } elseif ( $batch_processor->is_enqueued( self::class ) ) { $tools['stop_convert_order_coupon_data'] = array( 'name' => __( 'Stop converting order coupon data to the simplified format', 'woocommerce' ), 'button' => __( 'Stop converting', 'woocommerce' ), 'desc' => /* translators: %d=count of entries pending conversion */ sprintf( __( 'This will stop the background process that converts <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. There are currently %d entries that can be converted.', 'woocommerce' ), $pending_count ), 'callback' => array( $this, 'dequeue' ), ); } else { $tools['start_converting_order_coupon_data'] = array( 'name' => __( 'Convert order coupon data to the simplified format', 'woocommerce' ), 'button' => __( 'Start converting', 'woocommerce' ), 'desc' => /* translators: %d=count of entries pending conversion */ sprintf( __( 'This will convert <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. The conversion will happen overtime in the background (via Action Scheduler). There are currently %d entries that can be converted.', 'woocommerce' ), $pending_count ), 'callback' => array( $this, 'enqueue' ), ); } return $tools; } /** * Start the background process for coupon data conversion. * * @return string Informative string to show after the tool is triggered in UI. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function enqueue(): string { $batch_processor = wc_get_container()->get( BatchProcessingController::class ); if ( $batch_processor->is_enqueued( self::class ) ) { return __( 'Background process for coupon meta conversion already started, nothing done.', 'woocommerce' ); } $batch_processor->enqueue_processor( self::class ); return __( 'Background process for coupon meta conversion started', 'woocommerce' ); } /** * Stop the background process for coupon data conversion. * * @return string Informative string to show after the tool is triggered in UI. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function dequeue(): string { $batch_processor = wc_get_container()->get( BatchProcessingController::class ); if ( ! $batch_processor->is_enqueued( self::class ) ) { return __( 'Background process for coupon meta conversion not started, nothing done.', 'woocommerce' ); } $batch_processor->remove_processor( self::class ); return __( 'Background process for coupon meta conversion stopped', 'woocommerce' ); } } PK �[1]jM�r0 0 + FraudProtection/SessionClearanceManager.phpnu ��� <?php /** * SessionClearanceManager class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Manages session clearance state for fraud protection. * * This class handles the session status tracking for fraud protection decisions, * managing three possible states: pending, allowed, and blocked. It integrates * with WooCommerce sessions and uses the FraudProtectionController logging helper * to maintain consistent audit logs. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class SessionClearanceManager { /** * Session key for storing clearance status. */ private const SESSION_KEY = '_fraud_protection_clearance_status'; /** * Session status: pending clearance. */ public const STATUS_PENDING = 'pending'; /** * Session status: allowed. */ public const STATUS_ALLOWED = 'allowed'; /** * Session status: blocked. */ public const STATUS_BLOCKED = 'blocked'; /** * Default session status. */ public const DEFAULT_STATUS = self::STATUS_ALLOWED; /** * Check if the current session is allowed. * * @return bool True if session is allowed, false otherwise. */ public function is_session_allowed(): bool { $status = $this->get_session_status(); return self::STATUS_ALLOWED === $status; } /** * Check if the current session is blocked. * * @return bool True if session is blocked, false otherwise. */ public function is_session_blocked(): bool { $status = $this->get_session_status(); return self::STATUS_BLOCKED === $status; } /** * Mark the current session as allowed. * * @return void */ public function allow_session(): void { $this->set_session_status( self::STATUS_ALLOWED ); $this->log_session_update_event( 'allowed' ); } /** * Mark the current session as pending (challenge required). * * @return void */ public function challenge_session(): void { $this->set_session_status( self::STATUS_PENDING ); $this->log_session_update_event( 'challenged' ); } /** * Mark the current session as blocked. * * @return void */ public function block_session(): void { $this->set_session_status( self::STATUS_BLOCKED ); $this->log_session_update_event( 'blocked' ); $this->empty_cart(); } /** * Get the current session clearance status. * * @return string One of: pending, allowed, blocked. */ public function get_session_status(): string { if ( ! $this->is_session_available() ) { return self::DEFAULT_STATUS; } $status = WC()->session->get( self::SESSION_KEY, self::DEFAULT_STATUS ); // Validate status value - return default for invalid values. if ( ! in_array( $status, array( self::STATUS_PENDING, self::STATUS_ALLOWED, self::STATUS_BLOCKED ), true ) ) { return self::DEFAULT_STATUS; } return $status; } /** * Set the session clearance status. * * @param string $status One of: pending, allowed, blocked. * @return void */ private function set_session_status( string $status ): void { if ( ! $this->is_session_available() ) { return; } WC()->session->set( self::SESSION_KEY, $status ); // Ensure session cookie is set so the session persists across page loads. // This is important because fraud protection may set session status before // any cart action triggers the cookie to be set. // Skip cookie setting if headers have already been sent (e.g., in test environment). if ( WC()->session instanceof \WC_Session_Handler ) { WC()->session->set_customer_session_cookie( true ); } } /** * Reset the session clearance status to default (allowed). * * @return void */ public function reset_session(): void { $this->set_session_status( self::DEFAULT_STATUS ); } /** * Ensure cart and session are available. * * Loads cart if not already loaded, which initializes session for both * traditional (cookie) and Store API (token) flows. * * @return void */ public function ensure_cart_loaded(): void { if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) { WC()->call_function( 'wc_load_cart' ); } } /** * Check if WooCommerce session is available. * * @return bool True if session is available. */ private function is_session_available(): bool { $this->ensure_cart_loaded(); return WC()->session instanceof \WC_Session; } /** * Get a unique identifier for the current session. * * @return string Session identifier. */ public function get_session_id(): string { if ( ! $this->is_session_available() ) { return 'no-session'; } // Use or generate a stable session ID for tracking consistency. $fraud_customer_session_id = WC()->session->get( '_fraud_protection_customer_session_id' ); if ( ! $fraud_customer_session_id ) { $fraud_customer_session_id = WC()->call_function( 'wc_rand_hash', 'customer_', 30 ); WC()->session->set( '_fraud_protection_customer_session_id', $fraud_customer_session_id ); } return $fraud_customer_session_id; } /** * Empty the cart. * * @return void */ private function empty_cart(): void { if ( function_exists( 'WC' ) && WC()->cart ) { WC()->cart->empty_cart(); } } /** * Log a session update event using FraudProtectionController's logging helper. * * @param string $action The action taken (allowed, challenged, or blocked). * @return void */ private function log_session_update_event( string $action ): void { $session_id = $this->get_session_id(); $user_id = get_current_user_id(); $user_info = $user_id ? "User: {$user_id}" : 'User: guest'; $timestamp = current_time( 'mysql' ); $message = sprintf( 'Session updated: %s | %s | Action: %s | Timestamp: %s', $session_id, $user_info, $action, $timestamp ); FraudProtectionController::log( 'info', $message ); } } PK �[1]��U �U ( FraudProtection/SessionDataCollector.phpnu ��� <?php /** * SessionDataCollector class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Collects comprehensive session and order data for fraud protection analysis. * * This class provides manual data collection for fraud protection events, gathering * session, customer, order, address, and payment information in the exact nested format * required by the WPCOM fraud protection service. All data collection is designed to * degrade gracefully when fields are unavailable, ensuring checkout never fails due to * missing fraud protection data. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class SessionDataCollector { /** * SessionClearanceManager instance. * * @var SessionClearanceManager */ private SessionClearanceManager $session_clearance_manager; /** * Initialize with dependencies. * * @internal * * @param SessionClearanceManager $session_clearance_manager The session clearance manager instance. */ final public function init( SessionClearanceManager $session_clearance_manager ): void { $this->session_clearance_manager = $session_clearance_manager; } /** * Collect comprehensive session and order data for fraud protection. * * This method is called manually at specific points in the checkout/payment flow * to gather all relevant data for fraud analysis. It returns data in the nested * format expected by the WPCOM fraud protection service. * * @since 10.5.0 * * @param string|null $event_type Optional event type identifier (e.g., 'checkout_started', 'payment_attempt'). * @param array $event_data Optional event-specific additional context data (may include 'order_id'). * @return array Nested array containing all collected fraud protection data. */ public function collect( ?string $event_type = null, array $event_data = array() ): array { // Ensure cart and session are loaded. $this->session_clearance_manager->ensure_cart_loaded(); // Extract order ID from event_data if provided. // There seem to be no universal way to get order id from session data, so we may start with passing it as a parameter when calling this method. $order_id_from_event = $event_data['order_id'] ?? null; return array( 'event_type' => $event_type, 'timestamp' => gmdate( 'Y-m-d H:i:s' ), 'wc_version' => WC()->version, 'session' => $this->get_session_data(), 'customer' => $this->get_customer_data(), 'order' => $this->get_order_data( $order_id_from_event ), 'shipping_address' => $this->get_shipping_address(), 'billing_address' => $this->get_billing_address(), 'event_data' => $event_data, ); } /** * Get current billing country from customer data. * * Reuses the same logic as get_billing_address() but returns only the country. * Tries WC_Customer first, falls back to session data, with graceful error handling. * * @since 10.5.0 * * @return string|null Current billing country code or null if unavailable. */ public function get_current_billing_country(): ?string { try { if ( WC()->customer instanceof \WC_Customer ) { $country = WC()->customer->get_billing_country(); return ! empty( $country ) ? \sanitize_text_field( $country ) : null; } elseif ( WC()->session instanceof \WC_Session ) { $customer_data = WC()->session->get( 'customer' ); if ( is_array( $customer_data ) && ! empty( $customer_data['country'] ) ) { return \sanitize_text_field( $customer_data['country'] ); } } } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Graceful degradation. } return null; } /** * Get current shipping country from customer data. * * Reuses the same logic as get_shipping_address() but returns only the country. * Tries WC_Customer first, falls back to session data, with graceful error handling. * * @since 10.5.0 * * @return string|null Current shipping country code or null if unavailable. */ public function get_current_shipping_country(): ?string { try { if ( WC()->customer instanceof \WC_Customer ) { $country = WC()->customer->get_shipping_country(); return ! empty( $country ) ? \sanitize_text_field( $country ) : null; } elseif ( WC()->session instanceof \WC_Session ) { $customer_data = WC()->session->get( 'customer' ); if ( is_array( $customer_data ) && ! empty( $customer_data['shipping_country'] ) ) { return \sanitize_text_field( $customer_data['shipping_country'] ); } } } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Graceful degradation. } return null; } /** * Get session data including session ID, IP address, email, and user agent. * * Collects session identification and tracking data with graceful degradation * for unavailable fields. Email collection follows the fallback chain: * logged-in user email → session customer data → WC_Customer billing email. * * @since 10.5.0 * * @return array Session data array with 6 keys. */ private function get_session_data(): array { try { $session_id = $this->session_clearance_manager->get_session_id(); $ip_address = $this->get_ip_address(); $email = $this->get_email(); $user_agent = $this->get_user_agent(); /** * $is_user_session is flag that we have a real browser session vs API-based interaction. * We start with a very basic check, but we might need a more sophisticated way to detect it in the future. */ $is_user_session = 'no-session' !== $session_id; return array( 'session_id' => $session_id, 'ip_address' => $ip_address, 'email' => $email, 'ja3_hash' => null, 'user_agent' => $user_agent, 'is_user_session' => $is_user_session, ); } catch ( \Exception $e ) { // Graceful degradation - return structure with null values. return array( 'session_id' => null, 'ip_address' => null, 'email' => null, 'ja3_hash' => null, 'user_agent' => null, 'is_user_session' => false, ); } } /** * Get customer data including name, billing email, and order history. * * Collects customer identification and history data with graceful degradation. * Tries WC_Customer object first, then falls back to session data if values are empty. * Includes lifetime_order_count which counts all orders regardless of status. * * @since 10.5.0 * * @return array Customer data array with 4 keys. */ private function get_customer_data(): array { $customer_data = array( 'first_name' => null, 'last_name' => null, 'billing_email' => null, 'lifetime_order_count' => 0, ); try { $lifetime_order_count = 0; // Try WC_Customer object first. if ( WC()->customer instanceof \WC_Customer ) { if ( WC()->customer->get_id() > 0 ) { // We need to reload the customer so it uses the correct data store to count the orders. $customer = new \WC_Customer( WC()->customer->get_id() ); $lifetime_order_count = $customer->get_order_count(); } $customer_data = array_merge( $customer_data, array( 'first_name' => \sanitize_text_field( WC()->customer->get_billing_first_name() ), 'last_name' => \sanitize_text_field( WC()->customer->get_billing_last_name() ), 'billing_email' => \sanitize_email( \WC()->customer->get_billing_email() ), 'lifetime_order_count' => $lifetime_order_count, ) ); } elseif ( WC()->session instanceof \WC_Session ) { // Fallback to session customer data if WC_Customer not available. $customer_session_data = WC()->session->get( 'customer' ); if ( is_array( $customer_session_data ) ) { $customer_data = array_merge( $customer_data, array( 'first_name' => \sanitize_text_field( $customer_session_data['first_name'] ?? null ), 'last_name' => \sanitize_text_field( $customer_session_data['last_name'] ?? null ), 'billing_email' => \sanitize_email( $customer_session_data['email'] ?? null ), ) ); } } } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Graceful degradation - return as much data as possible. } return $customer_data; } /** * Get order data including totals, currency, cart hash, and cart items. * * Collects comprehensive order information from the cart with graceful degradation. * Calculates shipping_tax_rate from shipping tax and shipping total. Sets customer_id * to 'guest' for non-logged-in users. * * @since 10.5.0 * * @param int|null $order_id_from_event Optional order ID from event data. * @return array Order data array with 11 keys including items array. */ private function get_order_data( ?int $order_id_from_event = null ): array { try { // Initialize default values. $order_id = $order_id_from_event; $customer_id = 'guest'; $total = 0; $items_total = 0; $shipping_total = 0; $tax_total = 0; $shipping_tax_rate = null; $discount_total = 0; $currency = WC()->call_function( 'get_woocommerce_currency' ); $cart_hash = null; $items = array(); // Get customer ID from WooCommerce customer object if available. // We don't need to fallback to session data here, because customer id won't be stored there. if ( WC()->customer instanceof \WC_Customer ) { $id = WC()->customer->get_id(); if ( $id ) { $customer_id = $id; } } // Get cart data if available. if ( WC()->cart instanceof \WC_Cart ) { $items_total = (float) WC()->cart->get_subtotal(); $shipping_total = (float) WC()->cart->get_shipping_total(); $tax_total = (float) WC()->cart->get_cart_contents_tax(); $discount_total = (float) WC()->cart->get_discount_total(); $cart_hash = WC()->cart->get_cart_hash(); $items = $this->get_cart_items(); $total = (float) WC()->cart->get_total( 'edit' ); // Calculate shipping_tax_rate. $shipping_tax = (float) WC()->cart->get_shipping_tax(); if ( $shipping_total > 0 && $shipping_tax > 0 ) { $shipping_tax_rate = $shipping_tax / $shipping_total; } } return array( 'order_id' => $order_id, 'customer_id' => $customer_id, 'total' => $total, 'items_total' => $items_total, 'shipping_total' => $shipping_total, 'tax_total' => $tax_total, 'shipping_tax_rate' => $shipping_tax_rate, 'discount_total' => $discount_total, 'currency' => $currency, 'cart_hash' => $cart_hash, 'items' => $items, ); } catch ( \Exception $e ) { // Graceful degradation - return structure with default values. return array( 'order_id' => null, 'customer_id' => 'guest', 'total' => 0, 'items_total' => 0, 'shipping_total' => 0, 'tax_total' => 0, 'shipping_tax_rate' => null, 'discount_total' => 0, 'currency' => WC()->call_function( 'get_woocommerce_currency' ), 'cart_hash' => null, 'items' => array(), ); } } /** * Get cart items with detailed product information. * * Iterates through cart items and extracts comprehensive product data including * name, description, category, SKU, pricing, quantities, and WooCommerce-specific * attributes. Returns array of item objects with 12 fields each. * * @since 10.5.0 * * @return array Array of cart item objects with detailed product information. */ private function get_cart_items(): array { $items = array(); try { if ( ! WC()->cart instanceof \WC_Cart ) { return $items; } foreach ( WC()->cart->get_cart() as $cart_item ) { try { $product = $cart_item['data'] ?? null; if ( ! $product instanceof \WC_Product ) { continue; } $quantity = $cart_item['quantity'] ?? 1; // Calculate per-unit amounts. $unit_price = (float) $product->get_price(); $line_tax = $cart_item['line_tax'] ?? 0; $unit_tax_amount = $quantity > 0 ? ( (float) $line_tax / $quantity ) : 0; $line_discount = $cart_item['line_subtotal'] - $cart_item['line_total']; $unit_discount_amount = $quantity > 0 ? ( (float) $line_discount / $quantity ) : 0; $category = $this->get_product_category_names( $product ); $items[] = array( 'name' => $product->get_name() ? $product->get_name() : null, 'description' => $product->get_description() ? $product->get_description() : null, 'category' => $category, 'sku' => $product->get_sku() ? $product->get_sku() : null, 'quantity' => $quantity, 'unit_price' => $unit_price, 'unit_tax_amount' => $unit_tax_amount, 'unit_discount_amount' => $unit_discount_amount, 'product_type' => $product->get_type() ? $product->get_type() : null, 'is_virtual' => $product->is_virtual(), 'is_downloadable' => $product->is_downloadable(), 'attributes' => $product->get_attributes() ? $product->get_attributes() : array(), ); } catch ( \Exception $e ) { // Skip this item if there's an error, continue with next item. continue; } } } catch ( \Exception $e ) { // Return empty array on error. return array(); } return $items; } /** * Get billing address from customer data. * * Collects billing address fields from WC_Customer object with graceful degradation. * Returns array with 6 address fields, sanitized with sanitize_text_field(). * * @since 10.5.0 * * @return array Billing address array with 6 keys. */ private function get_billing_address(): array { $billing_data = array( 'first_name' => null, 'last_name' => null, 'address' => null, 'address_1' => null, 'address_2' => null, 'city' => null, 'state' => null, 'country' => null, 'phone' => null, 'postcode' => null, ); try { // Try WC_Customer object first. if ( WC()->customer instanceof \WC_Customer ) { $billing_data = array_merge( $billing_data, array( 'first_name' => \sanitize_text_field( WC()->customer->get_billing_first_name() ), 'last_name' => \sanitize_text_field( WC()->customer->get_billing_last_name() ), 'address_1' => \sanitize_text_field( WC()->customer->get_billing_address_1() ), 'address_2' => \sanitize_text_field( WC()->customer->get_billing_address_2() ), 'city' => \sanitize_text_field( WC()->customer->get_billing_city() ), 'state' => \sanitize_text_field( WC()->customer->get_billing_state() ), 'country' => \sanitize_text_field( WC()->customer->get_billing_country() ), 'phone' => \sanitize_text_field( WC()->customer->get_billing_phone() ), 'postcode' => \sanitize_text_field( WC()->customer->get_billing_postcode() ), ) ); } elseif ( WC()->session instanceof \WC_Session ) { // Fallback to session customer data if WC_Customer not available. $customer_data = WC()->session->get( 'customer' ); if ( is_array( $customer_data ) ) { $billing_data = array_merge( $billing_data, array( 'first_name' => \sanitize_text_field( $customer_data['first_name'] ?? null ), 'last_name' => \sanitize_text_field( $customer_data['last_name'] ?? null ), 'address' => \sanitize_text_field( $customer_data['address'] ?? null ), 'address_1' => \sanitize_text_field( $customer_data['address_1'] ?? null ), 'address_2' => \sanitize_text_field( $customer_data['address_2'] ?? null ), 'city' => \sanitize_text_field( $customer_data['city'] ?? null ), 'state' => \sanitize_text_field( $customer_data['state'] ?? null ), 'country' => \sanitize_text_field( $customer_data['country'] ?? null ), 'phone' => \sanitize_text_field( $customer_data['phone'] ?? null ), 'postcode' => \sanitize_text_field( $customer_data['postcode'] ?? null ), ) ); } } } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Graceful degradation - prevents any errors from being thrown. } return $billing_data; } /** * Get shipping address from customer data. * * Collects shipping address fields from WC_Customer object with graceful degradation. * Returns array with 6 address fields, sanitized with sanitize_text_field(). * * @since 10.5.0 * * @return array Shipping address array with 6 keys. */ private function get_shipping_address(): array { $shipping_data = array( 'first_name' => null, 'last_name' => null, 'address' => null, 'address_1' => null, 'address_2' => null, 'city' => null, 'state' => null, 'postcode' => null, 'country' => null, ); try { if ( WC()->customer instanceof \WC_Customer ) { $shipping_data = array_merge( $shipping_data, array( 'first_name' => \sanitize_text_field( WC()->customer->get_shipping_first_name() ), 'last_name' => \sanitize_text_field( WC()->customer->get_shipping_last_name() ), 'address_1' => \sanitize_text_field( WC()->customer->get_shipping_address_1() ), 'address_2' => \sanitize_text_field( WC()->customer->get_shipping_address_2() ), 'city' => \sanitize_text_field( WC()->customer->get_shipping_city() ), 'state' => \sanitize_text_field( WC()->customer->get_shipping_state() ), 'postcode' => \sanitize_text_field( WC()->customer->get_shipping_postcode() ), 'country' => \sanitize_text_field( WC()->customer->get_shipping_country() ), ) ); } elseif ( WC()->session instanceof \WC_Session ) { // Fallback to session customer data if WC_Customer not available. $customer_data = WC()->session->get( 'customer' ); if ( is_array( $customer_data ) ) { $shipping_data = array_merge( $shipping_data, array( 'first_name' => \sanitize_text_field( $customer_data['shipping_first_name'] ?? null ), 'last_name' => \sanitize_text_field( $customer_data['shipping_last_name'] ?? null ), 'address_1' => \sanitize_text_field( $customer_data['shipping_address_1'] ?? null ), 'address_2' => \sanitize_text_field( $customer_data['shipping_address_2'] ?? null ), 'city' => \sanitize_text_field( $customer_data['shipping_city'] ?? null ), 'state' => \sanitize_text_field( $customer_data['shipping_state'] ?? null ), 'postcode' => \sanitize_text_field( $customer_data['shipping_postcode'] ?? null ), 'country' => \sanitize_text_field( $customer_data['shipping_country'] ?? null ), ) ); } } } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Graceful degradation - returns as much data as possible. } return $shipping_data; } /** * Get client IP address using WooCommerce geolocation utility. * * @since 10.5.0 * * @return string|null IP address or null if not available. */ private function get_ip_address(): ?string { if ( class_exists( 'WC_Geolocation' ) ) { $ip = \WC_Geolocation::get_ip_address(); return $ip ? $ip : null; } return null; } /** * Get customer email with fallback chain. * * Tries logged-in user email first, then WC_Customer billing email, * then session customer data as fallback. * * @since 10.5.0 * * @return string|null Email address or null if not available. */ private function get_email(): ?string { // Try logged-in user first. if ( \is_user_logged_in() ) { $user = \wp_get_current_user(); if ( $user && $user->user_email ) { return \sanitize_email( $user->user_email ); } } // Try WC_Customer object. if ( WC()->customer instanceof \WC_Customer ) { $email = WC()->customer->get_billing_email(); if ( $email ) { return \sanitize_email( $email ); } } // Fallback to session customer data if WC_Customer not available. if ( WC()->session instanceof \WC_Session ) { $customer_data = WC()->session->get( 'customer' ); if ( is_array( $customer_data ) && ! empty( $customer_data['email'] ) ) { return \sanitize_email( $customer_data['email'] ); } } return null; } /** * Get user agent string from HTTP headers. * * @since 10.5.0 * * @return string|null User agent or null if not available. */ private function get_user_agent(): ?string { if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); } return null; } /** * Get product category names as comma-separated list. * * Uses WooCommerce helper with caching for better performance. * Returns all categories for the product, not just the primary one. * * @since 10.5.0 * * @param \WC_Product $product The product object. * @return string|null Comma-separated category names or null if none. */ private function get_product_category_names( \WC_Product $product ): ?string { $terms = WC()->call_function( 'wc_get_product_terms', $product->get_id(), 'product_cat' ); if ( empty( $terms ) || ! is_array( $terms ) ) { return null; } $category_names = array_map( function ( $term ) { return $term->name; }, $terms ); return implode( ', ', $category_names ); } } PK �[1]����| | , FraudProtection/JetpackConnectionManager.phpnu ��� <?php /** * JetpackConnectionManager class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection; defined( 'ABSPATH' ) || exit; /** * Manages Jetpack connection status and validation for fraud protection. * * Provides centralized methods to check connection status, validate requirements, * and handle connection-related errors gracefully. * * @since 10.5.0 */ class JetpackConnectionManager { /** * Get the Jetpack blog ID. * * @return int|null Blog ID if available, null otherwise. */ public function get_blog_id(): ?int { // Get blog ID from Jetpack options. $blog_id = \Jetpack_Options::get_option( 'id' ); return $blog_id ? (int) $blog_id : null; } /** * Get connection status with detailed error information. * * Returns an array with connection status and any error details. * * @return array { * Connection status information. * * @type bool $connected Whether the site is connected. * @type string $error Error message if not connected. * @type string $error_code Error code if not connected. * @type int $blog_id Blog ID if available. * } */ public function get_connection_status(): array { $status = array( 'connected' => false, 'error' => '', 'error_code' => '', 'blog_id' => null, ); // Check if connected. if ( ! JetpackConnection::get_manager()->is_connected() ) { $status['error'] = __( 'Site is not connected to WordPress.com. Please connect your site to enable fraud protection.', 'woocommerce' ); $status['error_code'] = 'not_connected'; return $status; } // Get blog ID. $blog_id = $this->get_blog_id(); if ( ! $blog_id ) { $status['error'] = __( 'Jetpack blog ID not found. Please reconnect your site to WordPress.com.', 'woocommerce' ); $status['error_code'] = 'no_blog_id'; return $status; } // All checks passed. $status['connected'] = true; $status['blog_id'] = $blog_id; return $status; } /** * Get the Jetpack authorization URL for connecting the site. * * @param string $redirect_url URL to redirect to after authorization. * @return string|null Authorization URL or null on error. */ public function get_authorization_url( string $redirect_url = '' ): ?string { // If no redirect URL provided, use current admin URL. if ( empty( $redirect_url ) ) { $redirect_url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' ); } $authorization_data = JetpackConnection::get_authorization_url( $redirect_url, 'woocommerce-fraud-protection' ); if ( ! $authorization_data['success'] ) { FraudProtectionController::log( 'error', 'Failed to get Jetpack authorization URL.', $authorization_data['errors'] ); return null; } return $authorization_data['url']; } } PK �[1]�!�� ( FraudProtection/AdminSettingsHandler.phpnu ��� <?php /** * AdminSettingsHandler class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Handles admin settings for fraud protection. * * @since 10.5.0 */ class AdminSettingsHandler { /** * Jetpack connection manager instance. * * @var JetpackConnectionManager */ private $connection_manager; /** * Register hooks. */ public function register(): void { add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_jetpack_connection_field' ), 100, 2 ); add_action( 'woocommerce_admin_field_jetpack_connection', array( $this, 'handle_output_jetpack_connection_field' ), 10, 1 ); } /** * Initialize the class with dependencies. * * @internal * * @param JetpackConnectionManager $connection_manager Jetpack connection manager instance. * @return void */ final public function init( JetpackConnectionManager $connection_manager ): void { $this->connection_manager = $connection_manager; } /** * Add Jetpack connection field to fraud protection settings. * * @internal * * @param array $settings Existing settings. * @param string $current_section Current section name. * @return array Modified settings. */ public function add_jetpack_connection_field( $settings, $current_section ): array { // Only add on the features section. if ( 'features' !== $current_section ) { return $settings; } // Check if field already exists to prevent duplicates. foreach ( $settings as $setting ) { if ( isset( $setting['id'] ) && 'woocommerce_fraud_protection_jetpack_connection' === $setting['id'] ) { return $settings; } } // Find the fraud_protection field and add Jetpack connection field after it. $new_settings = array(); foreach ( $settings as $setting ) { $new_settings[] = $setting; // Add Jetpack connection field after fraud_protection checkbox. if ( isset( $setting['id'] ) && 'woocommerce_feature_fraud_protection_enabled' === $setting['id'] ) { $new_settings[] = array( 'id' => 'woocommerce_fraud_protection_jetpack_connection', 'type' => 'jetpack_connection', 'title' => __( 'Jetpack Connection', 'woocommerce' ), 'desc' => __( 'Connect your site to Jetpack to enable fraud protection features.', 'woocommerce' ), ); } } return $new_settings; } /** * Output the Jetpack connection field. * * @internal * * @param array $value Field configuration. * @return void */ public function handle_output_jetpack_connection_field( $value ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found // Only show Jetpack connection when fraud protection is enabled. if ( 'yes' !== get_option( 'woocommerce_feature_fraud_protection_enabled', 'no' ) ) { return; } $this->output_jetpack_connection_status(); } /** * Output the Jetpack connection status and button. * * @internal * * @return void */ private function output_jetpack_connection_status(): void { // Get connection status from connection manager. $connection_status = $this->connection_manager->get_connection_status(); ?> <tr valign="top"> <th scope="row" class="titledesc"> <label><?php esc_html_e( 'Jetpack Connection', 'woocommerce' ); ?></label> </th> <td class="forminp forminp-button"> <?php if ( ! $connection_status['connected'] ) : ?> <?php // Get authorization URL for connecting. $redirect_url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' ); $connection_url = $this->connection_manager->get_authorization_url( $redirect_url ); // If we couldn't get authorization URL, show error message. if ( ! $connection_url ) : ?> <p class="description" style="color: #dc3232;"> <?php echo esc_html( $connection_status['error'] ); ?> </p> <?php else : ?> <a href="<?php echo esc_url( $connection_url ); ?>" class="button button-secondary jetpack_connection_button"> <?php esc_html_e( 'Connect to Jetpack', 'woocommerce' ); ?> </a> <p class="description"> <?php esc_html_e( 'Connect your site to Jetpack to enable fraud protection features.', 'woocommerce' ); ?> </p> <?php endif; ?> <?php else : ?> <span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span> <span><?php esc_html_e( 'Connected to Jetpack', 'woocommerce' ); ?></span> <p class="description"> <?php printf( /* translators: %d: Blog ID */ esc_html__( 'Site ID: %d', 'woocommerce' ), (int) $connection_status['blog_id'] ); ?> </p> <?php endif; ?> </td> </tr> <?php } } PK �[1]ّ�� � * FraudProtection/FraudProtectionTracker.phpnu ��� <?php /** * FraudProtectionTracker class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Centralized fraud protection event tracker. * * This class provides a unified interface for tracking fraud protection events. * It logs events for the fraud protection service using already-collected data. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class FraudProtectionTracker { /** * Track fraud protection event with already-collected data. * * This method accepts fully-collected event data (including session context) * and logs it for the fraud protection service. * * The method implements graceful degradation - any errors during tracking * will be logged but will not break the functionality. * * @param string $event_type Event type identifier (e.g., 'cart_item_added'). * @param array $collected_data Fully-collected event data including session context. * @return void */ public function track_event( string $event_type, array $collected_data ): void { try { // phpcs:ignore Generic.Commenting.Todo.TaskFound // TODO: Once EventTracker/API client is implemented (WOOSUBS-1249), call it here: // $event_tracker = wc_get_container()->get( EventTracker::class ); // $event_tracker->track( $event_type, $collected_data ); // // For now, log the event for debugging and verification. FraudProtectionController::log( 'info', sprintf( 'Fraud protection event tracked: %s | Session ID: %s', $event_type, $collected_data['session']['session_id'] ?? 'N/A' ), array( 'event_type' => $event_type, 'collected_data' => $collected_data, ) ); } catch ( \Exception $e ) { // Gracefully handle errors - fraud protection should never break functionality. FraudProtectionController::log( 'error', sprintf( 'Failed to track fraud protection event: %s | Error: %s', $event_type, $e->getMessage() ), array( 'event_type' => $event_type, 'exception' => $e, ) ); } } } PK �[1]��U FraudProtection/ApiClient.phpnu ��� <?php /** * ApiClient class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client; defined( 'ABSPATH' ) || exit; /** * Handles communication with the WPCOM fraud protection endpoint. * * Uses Jetpack Connection for authenticated requests to the WPCOM endpoint * to get fraud protection decisions (allow, block, or challenge). * * This class implements a fail-open pattern: if the endpoint is unreachable, * times out, or returns an error, it returns an "allow" decision to ensure * legitimate transactions are never blocked due to service issues. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class ApiClient { /** * Default timeout for API requests in seconds. */ private const DEFAULT_TIMEOUT = 30; /** * WPCOM API version. */ private const WPCOM_API_VERSION = '2'; /** * WPCOM fraud protection events endpoint path within Transact platform. */ private const EVENTS_ENDPOINT = 'transact/fraud_protection/events'; /** * Decision type: allow session. */ public const DECISION_ALLOW = 'allow'; /** * Decision type: block session. */ public const DECISION_BLOCK = 'block'; /** * Decision type: challenge session. */ public const DECISION_CHALLENGE = 'challenge'; /** * Valid decision values that can be returned by the API. * * @var array<string> */ public const VALID_DECISIONS = array( self::DECISION_ALLOW, self::DECISION_BLOCK, ); /** * Send a fraud protection event and get a decision from WPCOM endpoint. * * Implements fail-open pattern: if the endpoint is unreachable or times out, * returns "allow" decision and logs the error. * * @since 10.5.0 * * @param string $event_type Type of event being sent (e.g., 'cart_updated', 'checkout_started'). * @param array<string, mixed> $event_data Event data to send to the endpoint. * @return string Decision: "allow" or "block". */ public function send_event( string $event_type, array $event_data ): string { $payload = array_merge( array( 'event_type' => $event_type ), array_filter( $event_data, fn( $value ) => null !== $value ) ); FraudProtectionController::log( 'info', sprintf( 'Sending fraud protection event: %s', $event_type ), array( 'payload' => $payload ) ); $response = $this->make_request( 'POST', self::EVENTS_ENDPOINT, $payload ); if ( is_wp_error( $response ) ) { $error_data = $response->get_error_data() ?? array(); $error_data = is_array( $error_data ) ? $error_data : array( 'error' => $error_data ); FraudProtectionController::log( 'error', sprintf( 'Event track request failed: %s. Failing open with "allow" decision.', $response->get_error_message() ), $error_data ); return self::DECISION_ALLOW; } if ( ! isset( $response['decision'] ) ) { FraudProtectionController::log( 'error', 'Response missing "decision" field. Failing open with "allow" decision.', array( 'response' => $response ) ); return self::DECISION_ALLOW; } $decision = $response['decision']; if ( ! in_array( $decision, self::VALID_DECISIONS, true ) ) { FraudProtectionController::log( 'error', sprintf( 'Invalid decision value "%s". Failing open with "allow" decision.', $decision ), array( 'response' => $response ) ); return self::DECISION_ALLOW; } $session = is_array( $event_data['session'] ?? null ) ? $event_data['session'] : array(); $session_id = $session['session_id'] ?? 'unknown'; FraudProtectionController::log( 'info', sprintf( 'Fraud decision received: %s | Event: %s | Session: %s', $decision, $event_type, $session_id ), array( 'response' => $response ) ); return $decision; } /** * Make an HTTP request to a WPCOM endpoint via Jetpack Connection. * * @param string $method HTTP method (GET, POST, etc.). * @param string $path Endpoint path (relative to sites/{blog_id}/). * @param array<string, mixed> $payload Request payload. * @return array<string, mixed>|\WP_Error Parsed JSON response or WP_Error on failure. */ private function make_request( string $method, string $path, array $payload ) { if ( ! class_exists( Jetpack_Connection_Client::class ) ) { return new \WP_Error( 'jetpack_not_available', 'Jetpack Connection is not available' ); } $blog_id = $this->get_blog_id(); if ( ! $blog_id ) { return new \WP_Error( 'no_blog_id', 'Jetpack blog ID not found. Is the site connected to WordPress.com?' ); } $full_path = sprintf( 'sites/%d/%s', $blog_id, $path ); $body = \wp_json_encode( $payload ); if ( false === $body ) { return new \WP_Error( 'json_encode_error', 'Failed to encode payload', array( 'payload' => $payload ) ); } $response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog( $full_path, self::WPCOM_API_VERSION, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'method' => $method, 'timeout' => self::DEFAULT_TIMEOUT, ), $body, 'wpcom' ); if ( is_wp_error( $response ) ) { return $response; } /** * Type assertion for PHPStan - Jetpack returns array on success. * * @var array $response */ $response_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); $data = json_decode( $response_body, true ); if ( $response_code >= 300 ) { return new \WP_Error( 'api_error', sprintf( 'Endpoint %s returned status code %d', "$method $path", $response_code ), array( 'response' => JSON_ERROR_NONE === json_last_error() ? $data : $response_body ) ); } if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) { return new \WP_Error( 'json_decode_error', sprintf( 'Failed to decode JSON response: %s', json_last_error_msg() ), array( 'response' => $response_body ) ); } return $data; } /** * Get the Jetpack blog ID. * * @return int|false Blog ID or false if not available. */ private function get_blog_id() { if ( ! class_exists( \Jetpack_Options::class ) ) { return false; } return \Jetpack_Options::get_option( 'id' ); } } PK �[1]B7� � - FraudProtection/FraudProtectionController.phpnu ��� <?php /** * FraudProtectionController class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; use Automattic\WooCommerce\Internal\Features\FeaturesController; use Automattic\WooCommerce\Internal\RegisterHooksInterface; defined( 'ABSPATH' ) || exit; /** * Main controller for fraud protection features. * * This class orchestrates all fraud protection components and ensures * zero-impact when the feature flag is disabled. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class FraudProtectionController implements RegisterHooksInterface { /** * Features controller instance. * * @var FeaturesController */ private FeaturesController $features_controller; /** * Jetpack connection manager instance. * * @var JetpackConnectionManager */ private JetpackConnectionManager $connection_manager; /** * Blocked session notice instance. * * @var BlockedSessionNotice */ private BlockedSessionNotice $blocked_session_notice; /** * Register hooks. */ public function register(): void { add_action( 'init', array( $this, 'on_init' ) ); add_action( 'admin_notices', array( $this, 'on_admin_notices' ) ); } /** * Initialize the instance, runs when the instance is created by the dependency injection container. * * @internal * * @param FeaturesController $features_controller The instance of FeaturesController to use. * @param JetpackConnectionManager $connection_manager The instance of JetpackConnectionManager to use. * @param BlockedSessionNotice $blocked_session_notice The instance of BlockedSessionNotice to use. */ final public function init( FeaturesController $features_controller, JetpackConnectionManager $connection_manager, BlockedSessionNotice $blocked_session_notice ): void { $this->features_controller = $features_controller; $this->connection_manager = $connection_manager; $this->blocked_session_notice = $blocked_session_notice; } /** * Hook into WordPress on init. * * @internal */ public function on_init(): void { // Bail if the feature is not enabled. if ( ! $this->feature_is_enabled() ) { return; } $this->blocked_session_notice->register(); } /** * Display admin notice when Jetpack connection is not available. * * @internal */ public function on_admin_notices(): void { // Only show if feature is enabled. if ( ! $this->feature_is_enabled() ) { return; } // Only show on WooCommerce settings page. $screen = get_current_screen(); if ( ! $screen || 'woocommerce_page_wc-settings' !== $screen->id ) { return; } $connection_status = $this->connection_manager->get_connection_status(); if ( $connection_status['connected'] ) { return; } $settings_url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' ); ?> <div class="notice notice-warning is-dismissible"> <p> <strong><?php esc_html_e( 'Fraud protection warning:', 'woocommerce' ); ?></strong> <?php echo esc_html( $connection_status['error'] ); ?> </p> <p> <?php printf( /* translators: %s: Settings page URL */ wp_kses_post( __( 'Fraud protection will fail open and allow all sessions until connected. <a href="%s">Connect to Jetpack</a>', 'woocommerce' ) ), esc_url( $settings_url ) ); ?> </p> </div> <?php } /** * Check if fraud protection feature is enabled. * * This method can be used by other fraud protection classes to check * the feature flag status. Returns false (fail-open) if init hasn't run yet. * * @return bool True if enabled, false if not enabled or init hasn't run yet. */ public function feature_is_enabled(): bool { // Fail-open: don't block if init hasn't run yet to avoid FeaturesController translation notices. if ( ! did_action( 'init' ) ) { return false; } return $this->features_controller->feature_is_enabled( 'fraud_protection' ); } /** * Log helper method for consistent logging across all fraud protection components. * * This static method ensures all fraud protection logs are written with * the same 'woo-fraud-protection' source for easy filtering in WooCommerce logs. * * @param string $level Log level (emergency, alert, critical, error, warning, notice, info, debug). * @param string $message Log message. * @param array $context Optional context data. * * @return void */ public static function log( string $level, string $message, array $context = array() ): void { wc_get_logger()->log( $level, $message, array_merge( $context, array( 'source' => 'woo-fraud-protection' ) ) ); } } PK �[1]�*I�= = # FraudProtection/DecisionHandler.phpnu ��� <?php /** * DecisionHandler class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Handles fraud protection decision application. * * This class is responsible for: * - Applying extension override filters for whitelisting * - Coordinating with SessionClearanceManager to apply decisions * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class DecisionHandler { /** * Session clearance manager instance. * * @var SessionClearanceManager */ private SessionClearanceManager $session_manager; /** * Initialize with dependencies. * * @internal * * @param SessionClearanceManager $session_manager The session clearance manager instance. */ final public function init( SessionClearanceManager $session_manager ): void { $this->session_manager = $session_manager; } /** * Apply a fraud protection decision. * * This method processes a decision from the API, applies any override filters, * validates the result, and updates the session status accordingly. * * The input decision is expected to be pre-validated by ApiClient. * * The decision flow: * 1. Apply the `woocommerce_fraud_protection_decision` filter for overrides * 2. Validate the filtered decision (third-party filters may return invalid values) * 3. Update session status via SessionClearanceManager * * @since 10.5.0 * * @param string $decision The decision from the API (allow, block). * @param array<string, mixed> $session_data The session data that was sent to the API. * @return string The final applied decision after any filter overrides. */ public function apply_decision( string $decision, array $session_data ): string { // Validate input decision and fail open if invalid. if ( ! $this->is_valid_decision( $decision ) ) { FraudProtectionController::log( 'warning', sprintf( 'Invalid decision "%s" received. Defaulting to "allow".', $decision ), array( 'session_data' => $session_data ) ); $decision = ApiClient::DECISION_ALLOW; } $original_decision = $decision; /** * Filters the fraud protection decision before it is applied. * * This filter allows extensions to override fraud protection decisions * to implement custom whitelisting logic. Common use cases: * - Whitelist specific users (e.g., admins, trusted customers) * - Whitelist specific conditions (e.g., certain IP ranges, logged-in users) * - Integrate with external fraud detection services * * Note: This filter can only change the decision to ApiClient::VALID_DECISIONS. * Any other value will be rejected and the original decision will be used. * * @since 10.5.0 * * @param string $decision The decision from the API (allow, block). * @param array<string, mixed> $session_data The session data that was analyzed. */ $decision = apply_filters( 'woocommerce_fraud_protection_decision', $decision, $session_data ); // Validate filtered decision (third-party filters may return invalid values). if ( ! $this->is_valid_decision( $decision ) ) { FraudProtectionController::log( 'warning', sprintf( 'Filter `woocommerce_fraud_protection_decision` returned invalid decision "%s". Using original decision "%s".', $decision, $original_decision ), array( 'original_decision' => $original_decision, 'filtered_decision' => $decision, 'session_data' => $session_data, ) ); $decision = $original_decision; } // Log if decision was overridden. if ( $decision !== $original_decision ) { FraudProtectionController::log( 'info', sprintf( 'Decision overridden by filter `woocommerce_fraud_protection_decision`: "%s" -> "%s"', $original_decision, $decision ), array( 'original_decision' => $original_decision, 'final_decision' => $decision, 'session_data' => $session_data, ) ); } // Apply the decision to the session. $this->update_session_status( $decision ); return $decision; } /** * Check if a decision value is valid. * * @param mixed $decision The decision to validate. * @return bool True if valid, false otherwise. */ private function is_valid_decision( $decision ): bool { if ( ! is_string( $decision ) ) { return false; } return in_array( $decision, ApiClient::VALID_DECISIONS, true ); } /** * Update the session status based on the decision. * * @param string $decision The validated decision to apply. * @return void */ private function update_session_status( string $decision ): void { switch ( $decision ) { case ApiClient::DECISION_ALLOW: $this->session_manager->allow_session(); break; case ApiClient::DECISION_BLOCK: $this->session_manager->block_session(); break; } } } PK �[1]2�.Z - FraudProtection/FraudProtectionDispatcher.phpnu ��� <?php /** * FraudProtectionDispatcher class file. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\FraudProtection; defined( 'ABSPATH' ) || exit; /** * Centralized fraud protection event dispatcher. * * This class provides a unified interface for dispatching fraud protection events. * It coordinates data collection and transmission for fraud protection events by * orchestrating ApiClient and DecisionHandler components. * * @since 10.5.0 * @internal This class is part of the internal API and is subject to change without notice. */ class FraudProtectionDispatcher { /** * API client instance. * * @var ApiClient */ private ApiClient $api_client; /** * Decision handler instance. * * @var DecisionHandler */ private DecisionHandler $decision_handler; /** * Fraud protection controller instance. * * @var FraudProtectionController */ private FraudProtectionController $fraud_protection_controller; /** * Session data collector instance. * * @var SessionDataCollector */ private SessionDataCollector $data_collector; /** * Initialize with dependencies. * * @internal * * @param ApiClient $api_client The API client instance. * @param DecisionHandler $decision_handler The decision handler instance. * @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance. * @param SessionDataCollector $data_collector The session data collector instance. */ final public function init( ApiClient $api_client, DecisionHandler $decision_handler, FraudProtectionController $fraud_protection_controller, SessionDataCollector $data_collector ): void { $this->api_client = $api_client; $this->decision_handler = $decision_handler; $this->fraud_protection_controller = $fraud_protection_controller; $this->data_collector = $data_collector; } /** * Dispatch fraud protection event. * * This method collects session data and dispatches it to the fraud protection service. * It orchestrates the following flow: * 1. Check if feature is enabled (fail-open if not) * 2. Collect comprehensive session data via SessionDataCollector * 3. Apply extension data filter to allow custom data * 4. Send event to API and get decision * 5. Apply decision via DecisionHandler * * The method implements graceful degradation - any errors during tracking * will be logged but will not break the functionality. * * @param string $event_type Event type identifier (e.g., 'cart_item_added'). * @param array $event_data Optional event-specific data to include with session data. * @return void */ public function dispatch_event( string $event_type, array $event_data = array() ): void { try { // Check if feature is enabled - fail-open if not. if ( ! $this->fraud_protection_controller->feature_is_enabled() ) { FraudProtectionController::log( 'debug', sprintf( 'Fraud protection event not dispatched (feature disabled): %s', $event_type ), array( 'event_type' => $event_type ) ); return; } // Collect comprehensive session data. $collected_data = $this->data_collector->collect( $event_type, $event_data ); /** * Filters the fraud protection event data before sending to the API. * * This filter allows extensions to modify or add custom data to fraud protection * events. Common use cases include: * - Adding custom payment gateway data * - Adding subscription-specific context * - Adding custom risk signals * * @since 10.5.0 * * @param array $collected_data Fully-collected event data including session context. * @param string $event_type Event type identifier (e.g., 'cart_item_added'). */ $collected_data = apply_filters( 'woocommerce_fraud_protection_event_data', $collected_data, $event_type ); // Send event to API and get decision. $decision = $this->api_client->send_event( $event_type, $collected_data ); // Apply decision via DecisionHandler. $this->decision_handler->apply_decision( $decision, $collected_data ); } catch ( \Exception $e ) { // Gracefully handle errors - fraud protection should never break functionality. FraudProtectionController::log( 'error', sprintf( 'Failed to dispatch fraud protection event: %s | Error: %s', $event_type, $e->getMessage() ), array( 'event_type' => $event_type, 'exception' => $e, ) ); } } } PK �[1]� �gj"