enhanced it to use proxy instead of individual wrappers

This commit is contained in:
rjchow 2024-09-17 17:39:53 +10:00 committed by Chi-Hsuan Huang
parent 75e210fa4f
commit 8b71ef9c0d
3 changed files with 146 additions and 434 deletions

View File

@ -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;
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Logging;
use Automattic\Jetpack\Constants;
/**
* UseNonBuiltInFunctions Trait
*
* This trait creates a wrapper for non-built-in functions for safety.
*
* @since 9.4.0
* @package Automattic\WooCommerce\Internal\Logging
*/
class SafeGlobalFunctionProxy {
private static function maybe_load_missing_function($name) {
$function_map = array(
'wp_parse_url' => 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
)
);
}
}

View File

@ -1,413 +0,0 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Logging;
use Automattic\Jetpack\Constants;
/**
* UseNonBuiltInFunctions Trait
*
* This trait creates a wrapper for non-built-in functions for safety.
*
* @since 9.4.0
* @package Automattic\WooCommerce\Internal\Logging
*/
trait UseNonBuiltInFunctions {
/**
* Wp_parse_url wrapper.
*
* @param string $url The URL to parse.
* @param int $component The specific component to retrieve. Use one of the PHP
* predefined constants to specify which one.
* Defaults to -1 (= return all parts as an array).
* @return mixed False on parse failure; Array of URL components on success;
* When a specific component has been requested: null if the component
* doesn't exist in the given URL; a string or - in the case of
* PHP_URL_PORT - integer when it does. See parse_url()'s return values.
*/
private function wp_parse_url( $url, $component = -1 ) {
try {
if ( ! function_exists( 'wp_parse_url' ) ) {
require_once ABSPATH . WPINC . '/http.php';
}
return wp_parse_url( $url, $component );
} catch ( \Throwable $e ) {
$this->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
)
);
}
}