aspireupdate/includes/class-api-rewrite.php
costdev 4ff3421777 API_Rewrite: Specify hook priority in all *_filter() calls.
This ensures the filter is added and removed when expected.
2025-04-16 22:45:02 +01:00

367 lines
10 KiB
PHP

<?php
/**
* The Class for Rewriting the Default API.
*
* @package aspire-update
*/
namespace AspireUpdate;
/**
* The Class for Rewriting the Default API.
*/
class API_Rewrite {
/**
* The default API Host.
*
* @var string
*/
private $default_host = 'api.wordpress.org';
/**
* The redirected API Host.
*
* @var string
*/
private $redirected_host;
/**
* Disable SSL.
*
* @var boolean
*/
private $disable_ssl;
/**
* API Key.
*
* @var string
*/
private $api_key;
/**
* The Constructor.
*
* @param string $redirected_host The host to redirect to.
* @param boolean $disable_ssl Disable SSL.
* @param string $api_key The API key to use with the host.
*/
public function __construct( $redirected_host, $disable_ssl, $api_key ) {
if ( 'debug' === $redirected_host ) {
$this->redirected_host = $this->default_host;
} else {
$this->redirected_host = strtolower( $redirected_host );
}
$this->disable_ssl = $disable_ssl;
$this->api_key = $api_key;
if ( Admin_Settings::get_instance()->get_setting( 'enable', false ) ) {
add_filter( 'pre_http_request', [ $this, 'pre_http_request' ], PHP_INT_MAX, 3 );
}
}
/**
* Add Authorization Header if API Key found.
*
* @param array $args The HTTP request arguments.
*
* @return array $args The modified HTTP request arguments.
*/
private function add_authorization_header( $args ) {
if ( '' !== $this->api_key ) {
Debug::log_string( __( 'API Key Authorization header added.', 'aspireupdate' ) );
if ( ! isset( $args['headers'] ) ) {
$args['headers'] = [];
}
$args['headers']['Authorization'] = 'Bearer ' . $this->api_key;
}
return $args;
}
/**
* Add Accept JSON header if the request is not for a file asset.
*
* @param array $args The HTTP request arguments.
*
* @return array $args The modified HTTP request arguments.
*/
private function add_accept_json_header( $args, $url ) {
$path = wp_parse_url( $url, PHP_URL_PATH );
// Check if the URL points to a .php file or has no extension.
if ( $path && preg_match( '#/[^/]+(\.php|/)$#', $path ) ) {
Debug::log_string( __( 'Accept JSON Header added for API calls.', 'aspireupdate' ) );
if ( ! isset( $args['headers'] ) ) {
$args['headers'] = [];
}
$args['headers']['Accept'] = 'application/json';
}
return $args;
}
/**
* Adding cache buster parameter for AC beta test. Will remove this after Beta.
*
* @param string $url The URL.
*
* @return string $url The updated URL.
*/
private function add_cache_buster( $url ) {
Debug::log_string( __( 'Cache Buster Added to URL', 'aspireupdate' ) );
return add_query_arg( 'cache_buster', time(), $url );
}
/**
* Get non-API assets.
*
* @param string $asset_type The type of asset.
*
* @return array An associative array of non-API asset paths and data.
*/
private function get_non_api_assets( $asset_type ) {
switch ( $asset_type ) {
case 'plugins':
return array_filter(
get_plugins(),
static function ( $asset ) {
return ! empty( $asset['UpdateURI'] );
}
);
case 'themes':
return array_filter(
wp_get_themes(),
static function ( $asset ) {
return ! empty( $asset->get( 'UpdateURI' ) );
}
);
default:
return [];
}
}
/**
* Get the request type.
*
* @param string $url The URL.
*
* @return string The request type.
*/
private function get_request_type( $url ) {
if ( false !== stripos( $url, '/update-check/' ) ) {
return 'update';
}
if ( false !== stripos( $url, '/info/' ) ) {
return 'info';
}
return '';
}
/**
* Get the asset type.
*
* @param string $url The URL.
*
* @return string The asset type.
*/
private function get_asset_type( $url ) {
if ( false !== stripos( $url, '/plugins/' ) ) {
return 'plugins';
} elseif ( false !== stripos( $url, '/themes/' ) ) {
return 'themes';
}
return '';
}
/**
* Rewrite the API End points.
*
* @param mixed $response The response for the request.
* @param array $parsed_args The arguments for the request.
* @param string $url The URL for the request.
*
* @return mixed The response or false.
*/
public function pre_http_request( $response, $parsed_args, $url ) {
if (
isset( $this->default_host ) &&
( '' !== $this->default_host ) &&
isset( $this->redirected_host ) &&
( '' !== $this->redirected_host )
) {
if ( false !== strpos( $url, $this->default_host ) ) {
Debug::log_string( __( 'Default API Found: ', 'aspireupdate' ) . $url );
if ( false !== $response ) {
$admin_settings = \AspireUpdate\Admin_Settings::get_instance();
$compatibility = $admin_settings->get_setting( 'compatibility' );
if ( ! empty( $compatibility['skip_rewriting_on_existing_response'] ) ) {
Debug::log_string(
sprintf(
/* translators: 1: The options' name, 2: The constant's name, 3: The explicitly required value. */
__( 'API rewriting has been skipped because the response has already been changed. Enable the %1$s option or set "%1$s" in the %2$s constant to %3$s to continue with API rewriting in future.', 'aspireupdate' ),
'skip_rewriting_on_existing_response',
'AP_COMPATIBILITY',
'true'
)
);
return $response;
}
}
if ( false === filter_var( $this->redirected_host, FILTER_VALIDATE_URL ) ) {
$error_message = __( 'Your API host is not a valid URL.', 'aspireupdate' );
Debug::log_string(
sprintf(
/* translators: %s: The error message. */
__( 'Request Failed: %s', 'aspireupdate' ),
$error_message
)
);
return new \WP_Error( 'invalid_host', $error_message );
}
if ( $this->default_host !== $this->redirected_host ) {
if ( $this->disable_ssl ) {
Debug::log_string( __( 'SSL Verification Disabled', 'aspireupdate' ) );
$parsed_args['sslverify'] = false;
}
$parsed_args = $this->add_authorization_header( $parsed_args );
$parsed_args = $this->add_accept_json_header( $parsed_args, $url );
$url = $this->add_cache_buster( $url );
$protocol = wp_parse_url( $url, PHP_URL_SCHEME );
$updated_url = str_replace(
"{$protocol}://{$this->default_host}",
untrailingslashit( $this->redirected_host ),
$url
);
Debug::log_string( __( 'API Rerouted to: ', 'aspireupdate' ) . $updated_url );
Debug::log_request( $parsed_args );
$request_type = $this->get_request_type( $updated_url );
$asset_type = $this->get_asset_type( $updated_url );
$is_plugin_or_theme = $asset_type && in_array( $asset_type, [ 'plugins', 'themes' ], true );
$non_api_assets = [];
// Remove non-API assets from update requests.
if ( 'update' === $request_type && $is_plugin_or_theme ) {
// This is also used later to remove non-API assets from update responses.
$non_api_assets = $this->get_non_api_assets( $asset_type );
if ( ! empty( $parsed_args['body'][ $asset_type ] ) ) {
$assets = json_decode( $parsed_args['body'][ $asset_type ], true );
$api_assets = array_diff_key( $assets[ $asset_type ], $non_api_assets );
if ( $assets[ $asset_type ] !== $api_assets ) {
$assets[ $asset_type ] = $api_assets;
Debug::log_string(
sprintf(
/* translators: %s: The asset type. */
__( 'Removed non-API %s from the update request.', 'aspireupdate' ),
'plugins' === $asset_type ? __( 'plugins', 'aspireupdate' ) : __( 'themes', 'aspireupdate' )
)
);
}
$parsed_args['body'][ $asset_type ] = wp_json_encode( $assets );
}
}
/**
* Temporarily Unhook Filter to prevent recursion.
*/
remove_filter( 'pre_http_request', [ $this, 'pre_http_request' ], PHP_INT_MAX );
$response = wp_remote_request( $updated_url, $parsed_args );
add_filter( 'pre_http_request', [ $this, 'pre_http_request' ], PHP_INT_MAX, 3 );
Debug::log_response( $response );
$response_code = wp_remote_retrieve_response_code( $response );
$codes_to_pass_through = [
200,
404,
];
if ( ! in_array( $response_code, $codes_to_pass_through, true ) ) {
$message = wp_remote_retrieve_response_message( $response );
Debug::log_string(
sprintf(
/* translators: %s: The error message. */
__( 'Request Failed: %s', 'aspireupdate' ),
$message
)
);
return new \WP_Error( 'failed_request', $message );
}
if ( $is_plugin_or_theme ) {
// Remove non-API assets from update responses.
if ( 'update' === $request_type && ! empty( $non_api_assets ) ) {
$body = json_decode( $response['body'], true );
if ( ! empty( $body[ $asset_type ] ) ) {
$asset_paths = array_keys( $body[ $asset_type ] );
$removed = false;
foreach ( $asset_paths as $asset_path ) {
if ( array_key_exists( $asset_path, $non_api_assets ) ) {
unset( $body[ $asset_type ][ $asset_path ] );
$removed = true;
}
}
$response['body'] = wp_json_encode( $body );
if ( $removed ) {
Debug::log_string(
sprintf(
/* translators: %s: The asset type. */
__( 'Removed non-API %s from the update response.', 'aspireupdate' ),
'plugins' === $asset_type ? __( 'plugins', 'aspireupdate' ) : __( 'themes', 'aspireupdate' )
)
);
}
}
}
// Remove AspireUpdate from information responses.
if ( 'info' === $request_type && 'plugins' === $asset_type && false === stripos( $updated_url, 'slug' ) ) {
$body = json_decode( $response['body'], true );
if ( ! empty( $body[ $asset_type ] ) ) {
foreach ( $body[ $asset_type ] as $asset_key => $asset_data ) {
if ( isset( $asset_data['slug'] ) && 'aspireupdate' === strtolower( $asset_data['slug'] ) ) {
unset( $body[ $asset_type ][ $asset_key ] );
Debug::log_string(
sprintf(
/* translators: %s: AspireUpdate. */
__( 'Removed %s from the response.', 'aspireupdate' ),
'AspireUpdate'
)
);
/*
* Do not `break` in case more than one entry with
* the slug has been injected into the response.
*/
}
}
$response['body'] = wp_json_encode( $body );
}
}
}
return $response;
}
}
}
return $response;
}
}