Add logging and an admin notice for Legacy REST API usages (#41804)

* Add logging and admin noticing for rest api usages

Two new settings are added (UI in the Legacy API settings page):

- woocommerce_legacy_api_log_enabled
- woocommerce_legacy_api_usage_notice_enabled

When any of the two are enabled, legacy API usages are stored
in two options, 'wc_legacy_rest_usages' and 'wc_legacy_rest_last_usage'.

'wc_legacy_rest_usages' is a dictionary keyed by user agent,
each entry is in turn a dictionary keyed by request route,
items are arrays containing first and last usage dates as well
as total usages count (API version is logged too but for simplicity
it's not used to key the data).

'wc_legacy_rest_last_usage' contains the entry for the last usage,
regardless of user agent and route. It's used to display the notice.

When 'woocommerce_legacy_api_usage_notice_enabled' is enabled,
and the 'wc_legacy_rest_last_usage' option exists, an admin notice
displaying its contents is shown.

* Add changelog file

* Linting fixes

* Fix unit test

* Simplify the approach to logging/noticing:

- Remove settings
- Use a transient instead of an option for temporary data
- Store temporary data by user agent but not by route
- Make the admin notice dismissable
- Don't log now show the notice if the legacy REST API extension
  is installed and active
- Add a filter to explicitly disable the logging

* Small fixes, including a missing "exit" after request processing

* Apply suggestions from code review

Co-authored-by: Jorge A. Torres <jorge.torres@automattic.com>

* Fix linting issues

* Update the warning text under the "Enable legacy REST API" setting

* Change the rules to display the notice.

Now it won't appear if the transient isn't available
or if the Legacy REST API is disabled (or if the Legacy REST API
extension is active, as before); but if the user hasn't
explicitly dismissed the notice it will appear again if the transient
is recreated or the Legacy REST API is enabled again.

---------

Co-authored-by: Jorge A. Torres <jorge.torres@automattic.com>
This commit is contained in:
Néstor Soriano 2023-12-20 12:34:31 +01:00 committed by GitHub
parent 8853c93aca
commit df17713dea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 175 additions and 57 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add logging and an admin notice for Legacy REST API usages

View File

@ -280,6 +280,19 @@ class WC_Admin_Notices {
do_action( 'woocommerce_hide_' . $name . '_notice' );
}
/**
* Check if a given user has dismissed a given admin notice.
*
* @since 8.5.0
*
* @param string $name The name of the admin notice to check.
* @param int|null $user_id User id, or null for the current user.
* @return bool True if the user has dismissed the notice.
*/
public static function user_has_dismissed_notice( string $name, ?int $user_id = null ): bool {
return (bool) get_user_meta( $user_id ?? get_current_user_id(), "dismissed_{$name}_notice", true );
}
/**
* Add notices + styles if needed.
*/

View File

@ -393,11 +393,12 @@ class WC_Settings_Advanced extends WC_Settings_Page {
if ( ! is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) {
$enable_legacy_api_setting['desc_tip'] = sprintf(
// translators: Placeholder is a URL.
// translators: Placeholders are URLs.
__(
'⚠️ <b>The Legacy REST API will be removed in WooCommerce 9.0.</b> A separate WooCommerce extension will soon be available to keep it enabled. <b><a target="_blank" href="%s">Learn more about this change.</a></b>',
'⚠️ <b>The Legacy REST API will be removed in WooCommerce 9.0.</b> A separate WooCommerce extension will soon be available to keep it enabled. You can check Legacy REST API usages in <b><a target="_blank" href="%1$s">the WooCommerce log files</a></b> (file names start with <code>legacy_rest_api_usages</code>). <b><a target="_blank" href="%2$s">Learn more about this change.</a></b>',
'woocommerce'
),
admin_url( 'admin.php?page=wc-status&tab=logs' ),
'https://developer.woo.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/'
);
}

View File

@ -8,6 +8,8 @@
* @since 2.6
*/
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -17,6 +19,8 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class WC_Legacy_API {
use AccessiblePrivateMethods;
/**
* This is the major version for the REST API and takes
* first-order position in endpoint URLs.
@ -47,6 +51,8 @@ class WC_Legacy_API {
*/
public function init() {
add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 );
$this->mark_method_as_accessible( 'maybe_display_legacy_wc_api_usage_notice' );
self::add_action( 'admin_notices', array( $this, 'maybe_display_legacy_wc_api_usage_notice' ), 0 );
}
/**
@ -62,6 +68,56 @@ class WC_Legacy_API {
return $vars;
}
/**
* Write a log entry and update the last usage options, for a Legacy REST API request.
*
* @param string $route The Legacy REST API route requested.
* @param string|null $user_agent The content of the user agent HTTP header in the request, null if not available.
*/
private function maybe_log_rest_api_request( string $route, ?string $user_agent ) {
if ( is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) {
return;
}
$user_agent = $user_agent ?? 'unknown';
$current_date = wp_date( 'Y-m-d H:i:s' );
$stored_api_accesses = get_transient( 'wc_legacy_rest_api_usages' );
if ( false === $stored_api_accesses ) {
$stored_api_accesses = array(
'user_agents' => array(),
'first_usage' => $current_date,
'total_count' => 0,
);
}
$stored_api_accesses_for_user_agent = $stored_api_accesses['user_agents'][ $user_agent ] ?? null;
if ( is_null( $stored_api_accesses_for_user_agent ) ) {
$stored_api_accesses['user_agents'][ $user_agent ] = array(
'first_date' => $current_date,
'last_date' => $current_date,
'count' => 1,
);
} else {
$stored_api_accesses['user_agents'][ $user_agent ]['count']++;
$stored_api_accesses['user_agents'][ $user_agent ]['last_date'] = $current_date;
}
$stored_api_accesses['total_count']++;
set_transient( 'wc_legacy_rest_api_usages', $stored_api_accesses, time() + 2 * WEEK_IN_SECONDS );
/**
* This filter allows disabling the logging of Legacy REST API usages.
*
* @param bool $do_logging True to enable the logging of all the Legacy REST API usages (default), false to disable.
*
* @since 8.5.0
*/
if ( apply_filters( 'woocommerce_log_legacy_rest_api_usages', true ) ) {
wc_get_logger()->info( 'Version: ' . WC_API_REQUEST_VERSION . ", Route: $route, User agent: $user_agent", array( 'source' => 'legacy_rest_api_usages' ) );
}
}
/**
* Add new endpoints.
*
@ -90,30 +146,71 @@ class WC_Legacy_API {
$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'] ) ) {
return;
}
// REST API request.
if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) {
wc_maybe_define_constant( 'WC_API_REQUEST', true );
wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) );
wc_maybe_define_constant( 'WC_API_REQUEST', true );
wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) );
// Legacy v1 API request.
if ( 1 === WC_API_REQUEST_VERSION ) {
$this->handle_v1_rest_api_request();
} elseif ( 2 === WC_API_REQUEST_VERSION ) {
$this->handle_v2_rest_api_request();
} else {
$this->includes();
$route = $wp->query_vars['wc-api-route'];
$this->maybe_log_rest_api_request( $route, $_SERVER['HTTP_USER_AGENT'] ?? null );
$this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] );
// Legacy v1 API request.
if ( 1 === WC_API_REQUEST_VERSION ) {
$this->handle_v1_rest_api_request();
} elseif ( 2 === WC_API_REQUEST_VERSION ) {
$this->handle_v2_rest_api_request();
} else {
$this->includes();
// load API resource classes.
$this->register_resources( $this->server );
$this->server = new WC_API_Server( $route );
// Fire off the request.
$this->server->serve_request();
// load API resource classes.
$this->register_resources( $this->server );
// Fire off the request.
$this->server->serve_request();
}
exit;
}
/**
* Display an admin notice with information about the last Legacy REST API usage,
* if the corresponding transient is available and unless the Legacy REST API
* extension is installed or the user has dismissed the notice.
*/
private function maybe_display_legacy_wc_api_usage_notice(): void {
$legacy_api_usages = get_transient( 'wc_legacy_rest_api_usages' );
if ( false === $legacy_api_usages || is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) || 'yes' !== get_option( 'woocommerce_api_enabled' ) ) {
if ( WC_Admin_Notices::has_notice( 'legacy_api_usages_detected' ) ) {
WC_Admin_Notices::remove_notice( 'legacy_api_usages_detected' );
}
} elseif ( ! WC_Admin_Notices::user_has_dismissed_notice( 'legacy_api_usages_detected' ) ) {
unset( $legacy_api_usages['user_agents']['unknown'] );
$user_agents = array_keys( $legacy_api_usages['user_agents'] );
exit;
WC_Admin_Notices::add_custom_notice(
'legacy_api_usages_detected',
sprintf(
'%s%s',
sprintf(
'<h4>%s</h4>',
esc_html__( 'WooCommerce Legacy REST API access detected', 'woocommerce' )
),
sprintf(
// translators: %1$d = count of Legacy REST API usages recorded, %2$s = date and time of first access, %3$d = count of known user agents registered, %4$s = URL.
wpautop( wp_kses_data( __( '<p>The WooCommerce Legacy REST API has been accessed <b>%1$d</b> time(s) since <b>%2$s</b>. There are <b>%3$d</b> known user agent(s) registered. There are more details in <b><a target="_blank" href="%4$s">the WooCommerce log files</a></b> (file names start with <code>legacy_rest_api_usages</code>).', 'woocommerce' ) ) ),
$legacy_api_usages['total_count'],
$legacy_api_usages['first_usage'],
count( $user_agents ),
admin_url( 'admin.php?page=wc-status&tab=logs' ),
)
)
);
}
}
@ -126,23 +223,23 @@ class WC_Legacy_API {
public function includes() {
// API server / response handlers.
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' );
include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' );
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php';
// Authentication.
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' );
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' );
include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' );
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php';
include_once dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php';
// Allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
@ -157,7 +254,8 @@ class WC_Legacy_API {
*/
public function register_resources( $server ) {
$api_classes = apply_filters( 'woocommerce_api_classes',
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Coupons',
'WC_API_Customers',
@ -184,20 +282,20 @@ class WC_Legacy_API {
private function handle_v1_rest_api_request() {
// Include legacy required files for v1 REST API request.
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' );
include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' );
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php';
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' );
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' );
include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' );
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php';
// Allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
@ -205,7 +303,8 @@ class WC_Legacy_API {
$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );
// Register available resources for legacy v1 REST API request.
$api_classes = apply_filters( 'woocommerce_api_classes',
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Customers',
'WC_API_Orders',
@ -230,21 +329,21 @@ class WC_Legacy_API {
* @deprecated 2.6.0
*/
private function handle_v2_rest_api_request() {
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' );
include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' );
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php';
include_once dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php';
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' );
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php';
$this->authentication = new WC_API_Authentication();
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' );
include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' );
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php';
include_once dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php';
// allow plugins to load other response handlers or resource classes.
do_action( 'woocommerce_api_loaded' );
@ -252,7 +351,8 @@ class WC_Legacy_API {
$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );
// Register available resources for legacy v2 REST API request.
$api_classes = apply_filters( 'woocommerce_api_classes',
$api_classes = apply_filters(
'woocommerce_api_classes',
array(
'WC_API_Customers',
'WC_API_Orders',