diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php index 014eb17f748..8581efbe6e6 100644 --- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -23,7 +23,6 @@ use WC_Site_Tracking; * @package WooCommerce\Classes */ class RemoteLogger extends \WC_Log_Handler { - use UseNonBuiltInFunctions; const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; const RATE_LIMIT_ID = 'woocommerce_remote_logging'; @@ -51,7 +50,7 @@ class RemoteLogger extends \WC_Log_Handler { return $this->log( $level, $message, $context ); } catch ( \Throwable $e ) { // Log the error to the local logger so we can investigate. - $this->wc_get_logger()->error( 'Failed to handle the log: ' . $e->getMessage(), array( 'source' => 'remote-logging' ) ); + SafeGlobalFunctionProxy::wc_get_logger()->error( 'Failed to handle the log: ' . $e->getMessage(), array( 'source' => 'remote-logging' ) ); return false; } } @@ -75,14 +74,14 @@ class RemoteLogger extends \WC_Log_Handler { 'feature' => 'woocommerce_core', 'severity' => $level, 'message' => $this->sanitize( $message ), - 'host' => $this->wp_parse_url( $this->home_url(), PHP_URL_HOST ), + 'host' => SafeGlobalFunctionProxy::wp_parse_url( SafeGlobalFunctionProxy::home_url(), PHP_URL_HOST ) ?? 'Unable to retrieve host', 'tags' => array( 'woocommerce', 'php' ), 'properties' => array( - 'wc_version' => $this->get_wc_version(), + 'wc_version' => $this->get_wc_version(), // TODO check this 'php_version' => phpversion(), - 'wp_version' => $this->get_bloginfo( 'version' ), + 'wp_version' => SafeGlobalFunctionProxy::get_bloginfo( 'version' ) ?? 'Unable to retrieve wp version', 'request_uri' => $this->sanitize_request_uri( filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ), - 'store_id' => $this->get_option( \WC_Install::STORE_ID_OPTION, null ), + 'store_id' => SafeGlobalFunctionProxy::get_option( \WC_Install::STORE_ID_OPTION, null ) ?? 'Unable to retrieve store id', ), ); @@ -203,7 +202,7 @@ class RemoteLogger extends \WC_Log_Handler { if ( WC_Rate_Limiter::retried_too_soon( self::RATE_LIMIT_ID ) ) { // Log locally that the remote logging is throttled. - $this->wc_get_logger()->warning( 'Remote logging throttled.', array( 'source' => 'remote-logging' ) ); + SafeGlobalFunctionProxy::wc_get_logger()->warning( 'Remote logging throttled.', array( 'source' => 'remote-logging' ) ); return false; } @@ -230,7 +229,7 @@ class RemoteLogger extends \WC_Log_Handler { } $body = array( - 'params' => $this->wp_json_encode( $log_data ), + 'params' => SafeGlobalFunctionProxy::wp_json_encode( $log_data ) ?? 'Error occurred while encoding the log data', ); WC_Rate_Limiter::set_rate_limit( self::RATE_LIMIT_ID, self::RATE_LIMIT_DELAY ); @@ -239,10 +238,10 @@ class RemoteLogger extends \WC_Log_Handler { return false; } - $response = $this->wp_safe_remote_post( + $response = SafeGlobalFunctionProxy::wp_safe_remote_post( self::LOG_ENDPOINT, array( - 'body' => $this->wp_json_encode( $body ), + 'body' => SafeGlobalFunctionProxy::wp_json_encode( $body ) ?? 'Error occurred while encoding the log data', 'timeout' => 3, 'headers' => array( 'Content-Type' => 'application/json', @@ -251,8 +250,8 @@ class RemoteLogger extends \WC_Log_Handler { ) ); - if ( is_wp_error( $response ) ) { - $this->wc_get_logger()->error( 'Failed to send the log to the remote logging service: ' . $response->get_error_message(), array( 'source' => 'remote-logging' ) ); + if ( SafeGlobalFunctionProxy::is_wp_error( $response ) ) { // TODO: check this because the proxy doesn't return a WP_Error + SafeGlobalFunctionProxy::wc_get_logger()->error( 'Failed to send the log to the remote logging service: ' . $response->get_error_message(), array( 'source' => 'remote-logging' ) ); return false; } @@ -265,7 +264,7 @@ class RemoteLogger extends \WC_Log_Handler { * @return bool */ private function is_variant_assignment_allowed() { - $assignment = $this->get_option( 'woocommerce_remote_variant_assignment', 0 ); + $assignment = SafeGlobalFunctionProxy::get_option( 'woocommerce_remote_variant_assignment', 0 ) ?? 0; return ( $assignment <= 12 ); // Considering 10% of the 0-120 range. } @@ -275,12 +274,12 @@ class RemoteLogger extends \WC_Log_Handler { * @return bool */ private function should_current_version_be_logged() { - $new_version = $this->get_site_transient( self::WC_NEW_VERSION_TRANSIENT ); + $new_version = SafeGlobalFunctionProxy::get_site_transient( self::WC_NEW_VERSION_TRANSIENT ) ?? ''; if ( false === $new_version ) { $new_version = $this->fetch_new_woocommerce_version(); // Cache the new version for a week since we want to keep logging in with the same version for a while even if the new version is available. - $this->set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS ); + SafeGlobalFunctionProxy::set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS ); } if ( ! is_string( $new_version ) || '' === $new_version ) { @@ -289,7 +288,7 @@ class RemoteLogger extends \WC_Log_Handler { } // If the current version is the latest, we don't want to log errors. - return version_compare( $this->get_wc_version(), $new_version, '>=' ); + return version_compare( $this->get_wc_version(), $new_version, '>=' ); // TODO check this } /** @@ -350,7 +349,7 @@ class RemoteLogger extends \WC_Log_Handler { * @return string|null New version if an update is available, null otherwise. */ private function fetch_new_woocommerce_version() { - $plugin_updates = $this->get_plugin_updates(); + $plugin_updates = SafeGlobalFunctionProxy::get_plugin_updates(); // Check if WooCommerce plugin update information is available. if ( ! is_array( $plugin_updates ) || ! isset( $plugin_updates[ WC_PLUGIN_BASENAME ] ) ) { @@ -430,7 +429,7 @@ class RemoteLogger extends \WC_Log_Handler { $is_array_by_file = isset( $sanitized_trace[0]['file'] ); if ( $is_array_by_file ) { - return $this->wc_print_r( $sanitized_trace, true ); + return SafeGlobalFunctionProxy::wc_print_r( $sanitized_trace, true ); } return implode( "\n", $sanitized_trace ); @@ -444,7 +443,7 @@ class RemoteLogger extends \WC_Log_Handler { * @return bool */ protected function is_dev_or_local_environment() { - return in_array( $this->wp_get_environment_type(), array( 'development', 'local' ), true ); + return in_array( SafeGlobalFunctionProxy::wp_get_environment_type() ?? 'production', array( 'development', 'local' ), true ); } /** * Sanitize the request URI to only allow certain query parameters. @@ -475,8 +474,8 @@ class RemoteLogger extends \WC_Log_Handler { */ $whitelist = apply_filters( 'woocommerce_remote_logger_request_uri_whitelist', $default_whitelist ); - $parsed_url = $this->wp_parse_url( $request_uri ); - if ( ! isset( $parsed_url['query'] ) ) { + $parsed_url = SafeGlobalFunctionProxy::wp_parse_url( $request_uri ); + if ( ! isset( $parsed_url['query'] ) ) { // TODO: make sure this is failsafe return $request_uri; } diff --git a/plugins/woocommerce/src/Internal/Logging/SafeGlobalExecutionProxy.php b/plugins/woocommerce/src/Internal/Logging/SafeGlobalExecutionProxy.php new file mode 100644 index 00000000000..2994af06e79 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Logging/SafeGlobalExecutionProxy.php @@ -0,0 +1,126 @@ + ABSPATH . WPINC . '/http.php', + 'home_url' => ABSPATH . WPINC . '/link-template.php', + 'get_bloginfo' => ABSPATH . WPINC . '/general-template.php', + 'get_option' => ABSPATH . WPINC . '/option.php', + 'get_site_transient' => ABSPATH . WPINC . '/option.php', + 'set_site_transient' => ABSPATH . WPINC . '/option.php', + 'wp_safe_remote_post' => ABSPATH . WPINC . '/http.php', + 'is_wp_error' => ABSPATH . WPINC . '/load.php', + 'get_plugin_updates' => array( ABSPATH . 'wp-admin/includes/update.php', ABSPATH . 'wp-admin/includes/plugin.php' ), + 'wp_get_environment_type' => ABSPATH . WPINC . '/load.php', + 'wp_json_encode' => ABSPATH . WPINC . '/functions.php', + 'wc_get_logger' => WC_ABSPATH . 'includes/class-wc-logger.php', + 'wc_print_r' => WC_ABSPATH . 'includes/wc-core-functions.php', + ); + + if ( ! function_exists( $name ) ) { + if ( isset( $function_map[ $name ] ) ) { + $files = (array) $function_map[ $name ]; + foreach ($files as $file) { + require_once $file; + } + } else { + throw new Exception("Function $name does not exist and could not be loaded."); + } + } + } + + /** + * Proxy for trapping all calls on SafeGlobalFunctionProxy. + * Use this for calling WP and WC global functions safely. + * Example usage: + * + * SafeGlobalFunctionProxy::wp_parse_url('https://example.com', PHP_URL_PATH); + * + * @since 9.4.0 + * @param string $name The name of the function to call. + * @param array $arguments The arguments to pass to the function. + * @return mixed The result of the function call, or null if an error occurs. + */ + public static function __callStatic($name, $arguments) { + set_error_handler(static function (int $type, string $message, string $file, int $line) { + if (__FILE__ === $file) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $file = $trace[2]['file'] ?? $file; + $line = $trace[2]['line'] ?? $line; + } + throw new ErrorException($message, 0, $type, $file, $line); + }); + + try { + self::maybe_load_missing_function($name); + $results = call_user_func_array($name, $arguments); + } catch (Throwable $e) { + self::log_wrapper_error($name, $e->getMessage(), $arguments); + $results = null; + } finally { + restore_error_handler(); + } + + return $results; + } + + /** + * Get_wc_version wrapper. + * + * @return string The WooCommerce version. + * + * @throws \Exception If get_wc_version function does not exist. + */ + protected function get_wc_version() { + try { + return Constants::get_constant( 'WC_VERSION' ) ?? 'unknown'; + } catch ( \Throwable $e ) { + self::log_wrapper_error( + __FUNCTION__, + $e->getMessage(), + array() + ); + + return 'Unable to retrieve'; + } + } + + /** + * Log wrapper function errors to "local logging" for debugging. + * + * @param string $function_name The name of the wrapped function. + * @param string $error_message The error message. + * @param array $context Additional context for the error. + */ + protected function log_wrapper_error( $function_name, $error_message, $context = array() ) { + self::maybe_load_missing_function('wc_get_logger'); + + wc_get_logger()->error( + '[Wrapper function error] ' . sprintf( 'Error in %s: %s', $function_name, $error_message ), + array_merge( + array( + 'function' => $function_name, + 'source' => 'remote-logging', + ), + $context + ) + ); + } +} diff --git a/plugins/woocommerce/src/Internal/Logging/UseNonBuiltInFunctions.php b/plugins/woocommerce/src/Internal/Logging/UseNonBuiltInFunctions.php deleted file mode 100644 index 5fc4ade4237..00000000000 --- a/plugins/woocommerce/src/Internal/Logging/UseNonBuiltInFunctions.php +++ /dev/null @@ -1,413 +0,0 @@ -log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'url' => $url, - 'component' => $component, - ) - ); - - return 'Unable to retrieve'; - } - } - - /** - * Home_url wrapper. - * - * @param string $path The path to append to the home URL. - * @param string $scheme The scheme to use for the home URL. - * @return string The home URL with the path appended. - */ - private function home_url( $path = '', $scheme = null ) { - try { - if ( ! function_exists( 'home_url' ) ) { - require_once ABSPATH . WPINC . '/link-template.php'; - } - - return home_url( $path, $scheme ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'path' => $path, - 'scheme' => $scheme, - ) - ); - - return 'Unable to retrieve'; - } - } - - - /** - * Get_bloginfo wrapper. - * - * @param string $show Optional. Site info to retrieve. Default empty (site name). - * @param string $filter Optional. How to filter what is retrieved. Default 'raw'. - * @return string Mostly string values, might be empty. - */ - private function get_bloginfo( $show = '', $filter = 'raw' ) { - try { - if ( ! function_exists( 'get_bloginfo' ) ) { - require_once ABSPATH . WPINC . '/general-template.php'; - } - - return get_bloginfo( $show, $filter ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'show' => $show, - 'filter' => $filter, - ) - ); - - return 'Unable to retrieve'; - } - } - - /** - * Get_option wrapper. - * - * @param string $option Name of the option to retrieve. Expected to not be SQL-escaped. - * @param mixed $_default Optional. Default value to return if the option does not exist. - * @return mixed Value of the option. A value of any type may be returned, including - * scalar (string, boolean, float, integer), null, array, object. - * Scalar and null values will be returned as strings as long as they originate - * from a database stored option value. If there is no option in the database, - * boolean `false` is returned. - */ - private function get_option( $option, $_default = false ) { - try { - if ( ! function_exists( 'get_option' ) ) { - require_once ABSPATH . WPINC . '/option.php'; - } - - return get_option( $option, $_default ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'option' => $option, - '_default' => $_default, - ) - ); - return false; - } - } - - - /** - * Get_site_transient wrapper. - * - * @param string $transient Transient name. Expected to not be SQL-escaped. - * @return mixed Value of the transient. - */ - private function get_site_transient( $transient ) { - if ( ! function_exists( 'get_site_transient' ) ) { - require_once ABSPATH . WPINC . '/option.php'; - } - - try { - return get_site_transient( $transient ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'transient' => $transient, - ) - ); - return false; - } - } - - /** - * Set_site_transient wrapper. - * - * @param string $transient Transient name. Expected to not be SQL-escaped. - * @param mixed $value Value of the transient. - * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). - * @return bool True if the transient was set successfully, false otherwise. - */ - private function set_site_transient( $transient, $value, $expiration = 0 ) { - try { - if ( ! function_exists( 'set_site_transient' ) ) { - require_once ABSPATH . WPINC . '/option.php'; - } - - return set_site_transient( $transient, $value, $expiration ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'transient' => $transient, - 'value' => $value, - 'expiration' => $expiration, - ) - ); - return false; - } - } - - /** - * Wp_safe_remote_post wrapper. - * - * @param string $url URL to send the request to. - * @param array $args Optional. Additional arguments for the request. - * @return array|WP_Error The response or WP_Error on failure. - * - * @throws \Exception If wp_safe_remote_post function does not exist. - */ - private function wp_safe_remote_post( $url, $args = array() ) { - try { - if ( ! function_exists( 'wp_safe_remote_post' ) ) { - require_once ABSPATH . WPINC . '/http.php'; - } - - return wp_safe_remote_post( $url, $args ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'url' => $url, - 'args' => $args, - ) - ); - - return new \WP_Error( 'remote_post_error', $e->getMessage() ); - } - } - - - /** - * Is_wp_error wrapper. - * - * @param mixed $thing The variable to check. - * @return bool Whether the variable is an instance of WP_Error. - * - * @throws \Exception If is_wp_error function does not exist. - */ - private function is_wp_error( $thing ) { - try { - if ( ! function_exists( 'is_wp_error' ) ) { - require_once ABSPATH . WPINC . '/load.php'; - } - - return is_wp_error( $thing ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'thing' => $thing, - ) - ); - - // We can't determine if the variable is an instance of WP_Error so we throw an exception. - throw new \Exception( 'is_wp_error function does not exist' ); - } - } - - /** - * Get_plugin_updates wrapper. - * - * @return array|null The plugin updates array or null if not available. - */ - private function get_plugin_updates() { - try { - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - if ( ! function_exists( 'get_plugin_updates' ) ) { - require_once ABSPATH . 'wp-admin/includes/update.php'; - } - - return get_plugin_updates(); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array() - ); - - // If the function does not exist, return null since the update is not available. - return null; - } - } - - /** - * Get_wp_environment_type wrapper. - * - * @return string The environment type. - * - * @throws \Exception If wp_get_environment_type function does not exist. - */ - private function wp_get_environment_type() { - try { - if ( ! function_exists( 'wp_get_environment_type' ) ) { - require_once ABSPATH . WPINC . '/load.php'; - } - - return wp_get_environment_type(); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array() - ); - - // If the function does not exist, return production since the wp default value is production. - return 'production'; - } - } - - /** - * Wp_json_encode wrapper. - * - * @param mixed $data The data to encode. - * @return string The JSON encoded string. - * - * @throws \Exception If wp_json_encode function does not exist. - */ - private function wp_json_encode( $data ) { - try { - if ( ! function_exists( 'wp_json_encode' ) ) { - require_once ABSPATH . WPINC . '/functions.php'; - } - - return wp_json_encode( $data ); - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array( - 'data' => $data, - ) - ); - - return 'Unable to encode'; - } - } - - /** - * Get_wc_version wrapper. - * - * @return string The WooCommerce version. - * - * @throws \Exception If get_wc_version function does not exist. - */ - private function get_wc_version() { - try { - return Constants::get_constant( 'WC_VERSION' ) ?? 'unknown'; - } catch ( \Throwable $e ) { - $this->log_wrapper_error( - __FUNCTION__, - $e->getMessage(), - array() - ); - - return 'Unable to retrieve'; - } - } - - /** - * Wc_get_logger wrapper. - * - * @return \WC_Logger The WooCommerce logger. - * - * @throws \Exception If wc_get_logger function does not exist. - */ - private function wc_get_logger() { - try { - if ( ! function_exists( 'wc_get_logger' ) ) { - require_once WC_ABSPATH . 'includes/class-wc-logger.php'; - } - - return wc_get_logger(); - } catch ( \Throwable $e ) { - throw new \Exception( 'wc_get_logger function does not exist' ); - } - } - - /** - * Wc_print_r wrapper. - * - * @param mixed $data The data to print. - * @param bool $_return Whether to return the output instead of printing it. - * @return string The printed data. - */ - private function wc_print_r( $data, $_return = false ) { - if ( ! function_exists( 'wc_print_r' ) ) { - require_once WC_ABSPATH . 'includes/wc-core-functions.php'; - } - - return wc_print_r( $data, $_return ); - } - - - /** - * Log wrapper function errors to "local logging" for debugging. - * - * @param string $function_name The name of the wrapped function. - * @param string $error_message The error message. - * @param array $context Additional context for the error. - */ - private function log_wrapper_error( $function_name, $error_message, $context = array() ) { - $this->wc_get_logger()->error( - '[Wrapper function error] ' . sprintf( 'Error in %s: %s', $function_name, $error_message ), - array_merge( - array( - 'function' => $function_name, - 'source' => 'remote-logging', - ), - $context - ) - ); - } -}