updatepulse-server/inc/api/class-update-api.php
2025-03-18 17:46:51 +08:00

600 lines
16 KiB
PHP

<?php
namespace Anyape\UpdatePulse\Server\API;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
use Anyape\UpdatePulse\Server\Manager\Data_Manager;
use Anyape\UpdatePulse\Server\Scheduler\Scheduler;
use Anyape\Utils\Utils;
/**
* Update API class
*
* @since 1.0.0
*/
class Update_API {
/**
* Is doing API request
*
* @var bool|null
* @since 1.0.0
*/
protected static $doing_api_request = null;
/**
* Instance
*
* @var Update_API|null
* @since 1.0.0
*/
protected static $instance;
/**
* Update server object
*
* @var object|null
* @since 1.0.0
*/
protected $update_server;
/**
* Constructor
*
* @param boolean $init_hooks Whether to initialize hooks.
* @since 1.0.0
*/
public function __construct( $init_hooks = false ) {
if ( $init_hooks ) {
add_action( 'init', array( $this, 'add_endpoints' ), 10, 0 );
add_action( 'parse_request', array( $this, 'parse_request' ), -99, 0 );
add_action( 'upserv_checked_remote_package_update', array( $this, 'upserv_checked_remote_package_update' ), 10, 3 );
add_action( 'upserv_removed_package', array( $this, 'upserv_removed_package' ), 10, 3 );
add_action( 'upserv_registered_package_from_vcs', array( $this, 'upserv_registered_package_from_vcs' ), 10, 2 );
add_filter( 'query_vars', array( $this, 'query_vars' ), -99, 1 );
add_filter( 'puc_request_info_pre_filter', array( $this, 'puc_request_info_pre_filter' ), 10, 4 );
add_filter( 'puc_request_info_result', array( $this, 'puc_request_info_result' ), 10, 4 );
}
}
/*******************************************************************
* Public methods
*******************************************************************/
// WordPress hooks ---------------------------------------------
/**
* Add API endpoints
*
* Register the rewrite rules for the Update API endpoints.
*
* @since 1.0.0
*/
public function add_endpoints() {
add_rewrite_rule(
'^updatepulse-server-update-api/*$',
'index.php?$matches[1]&__upserv_update_api=1&',
'top'
);
}
/**
* Parse API requests
*
* Handle incoming API requests to the Update API endpoints.
*
* @since 1.0.0
*/
public function parse_request() {
global $wp;
if ( isset( $wp->query_vars['__upserv_update_api'] ) ) {
$this->handle_api_request();
}
}
/**
* Register query variables
*
* Add custom query variables used by the Update API.
*
* @param array $query_vars Existing query variables.
* @return array Modified query variables.
* @since 1.0.0
*/
public function query_vars( $query_vars ) {
$query_vars = array_merge(
$query_vars,
array(
'__upserv_update_api',
'action',
'token',
'package_id',
'update_type',
)
);
return $query_vars;
}
/**
* Handle checked remote package update event
*
* Actions to perform when a remote package update has been checked.
*
* @param bool $needs_update Whether the package needs an update.
* @param string $type The type of the package.
* @param string $slug The slug of the package.
* @since 1.0.0
*/
public function upserv_checked_remote_package_update( $needs_update, $type, $slug ) {
$this->schedule_check_remote_event( $slug );
}
/**
* Handle package registered from VCS event
*
* Actions to perform when a package has been registered from VCS.
*
* @param bool $result The result of the registration.
* @param string $slug The slug of the package.
* @since 1.0.0
*/
public function upserv_registered_package_from_vcs( $result, $slug ) {
if ( $result ) {
$this->schedule_check_remote_event( $slug );
}
}
/**
* Handle package removed event
*
* Actions to perform when a package has been removed.
*
* @param bool $result The result of the removal.
* @param string $type The type of the package.
* @param string $slug The slug of the package.
* @since 1.0.0
*/
public function upserv_removed_package( $result, $type, $slug ) {
if ( $result ) {
Scheduler::get_instance()->unschedule_all_actions( 'upserv_check_remote_' . $slug );
}
}
/**
* Pre-filter package information
*
* Filter package information before the update check.
*
* @param array $info Package information.
* @param object $api_obj The API object.
* @param mixed $ref Reference value.
* @param object $update_checker The update checker object.
* @return array Filtered package information.
* @since 1.0.0
*/
public function puc_request_info_pre_filter( $info, $api_obj, $ref, $update_checker ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
$vcs_config = upserv_get_package_vcs_config( $info['slug'] );
if ( empty( $vcs_config ) ) {
return $info;
}
/**
* Filter whether to filter the packages retrieved from the Version Control System.
*
* @param bool $filter_packages Whether to filter the packages retrieved from the Version Control System.
* @param array $info The information of the package from the VCS.
* @since 1.0.0
*/
$filter_packages = apply_filters(
'upserv_vcs_filter_packages',
$vcs_config['filter_packages'],
$info
);
$this->init_server( $info['slug'] );
if ( $this->update_server && $filter_packages ) {
$info = $this->update_server->pre_filter_package_info( $info, $api_obj, $ref );
}
return $info;
}
/**
* Filter package information result
*
* Filter package information after the update check.
*
* @param array $info Package information.
* @param object $api_obj The API object.
* @param mixed $ref Reference value.
* @param object $checker The update checker object.
* @return array Filtered package information.
* @since 1.0.0
*/
public function puc_request_info_result( $info, $api_obj, $ref, $checker ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
$vcs_config = upserv_get_package_vcs_config( $info['slug'] );
if ( empty( $vcs_config ) ) {
return $info;
}
/**
* Filter whether to filter the packages retrieved from the Version Control System.
*
* @param bool $filter_packages Whether to filter the packages retrieved from the Version Control System.
* @param array $info The information of the package from the VCS.
* @since 1.0.0
*/
$filter_packages = apply_filters(
'upserv_vcs_filter_packages',
$vcs_config['filter_packages'],
$info
);
$this->init_server( $info['slug'] );
if ( $this->update_server && $filter_packages ) {
$info = $this->update_server->filter_package_info( $info );
}
return $info;
}
// Misc. -------------------------------------------------------
/**
* Check if currently processing an API request
*
* Determine whether the current request is an Update API request.
*
* @return bool Whether the current request is an Update API request.
* @since 1.0.0
*/
public static function is_doing_api_request() {
if ( null === self::$doing_api_request ) {
self::$doing_api_request = Utils::is_url_subpath_match( '/^updatepulse-server-update-api$/' );
}
return self::$doing_api_request;
}
/**
* Get Update API instance
*
* Retrieve or create the Update API singleton instance.
*
* @return Update_API The Update API instance.
* @since 1.0.0
*/
public static function get_instance() {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Check for remote package updates
*
* Verify if a remote package has updates available.
*
* @param string $slug The package slug.
* @param string $type The package type.
* @return bool|mixed Result of the remote update check.
* @since 1.0.0
*/
public function check_remote_update( $slug, $type ) {
$this->init_server( $slug );
if ( ! $this->update_server ) {
return false;
}
$this->update_server->set_type( $type );
return $this->update_server->check_remote_package_update( $slug );
}
/**
* Download a remote package
*
* Download and process a package from a remote source.
*
* @param string $slug The package slug.
* @param string|null $type The package type.
* @param bool $force Whether to force the download.
* @return bool Whether the download was successful.
* @since 1.0.0
*/
public function download_remote_package( $slug, $type = null, $force = false ) {
$result = false;
if ( ! $type ) {
$types = array( 'plugin', 'theme', 'generic' );
foreach ( $types as $type ) {
$result = $this->download_remote_package( $slug, $type, $force );
if ( $result ) {
break;
}
}
return $result;
}
$this->init_server( $slug );
if ( ! $this->update_server ) {
return false;
}
$this->update_server->set_type( $type );
if ( $force || $this->update_server->check_remote_package_update( $slug ) ) {
$result = $this->update_server->save_remote_package_to_local( $slug, $force );
}
if ( $result ) {
if ( ! upserv_is_package_whitelisted( $slug ) ) {
upserv_whitelist_package( $slug );
}
$meta = upserv_get_package_metadata( $slug );
$meta['vcs'] = trailingslashit( $this->update_server->get_vcs_url() );
$meta['branch'] = $this->update_server->get_branch();
$meta['type'] = $type;
$meta['vcs_key'] = hash( 'sha256', trailingslashit( $meta['vcs'] ) . '|' . $meta['branch'] );
$meta['origin'] = 'vcs';
upserv_set_package_metadata( $slug, $meta );
}
return $result;
}
/*******************************************************************
* Protected methods
*******************************************************************/
/**
* Schedule remote check event
*
* Set up a scheduled event to check for remote package updates.
*
* @param string $slug The package slug.
* @since 1.0.0
*/
protected function schedule_check_remote_event( $slug ) {
$vcs_config = upserv_get_package_vcs_config( $slug );
if (
! upserv_get_option( 'use_vcs', 0 ) ||
empty( $vcs_config ) ||
(
isset( $vcs_config['use_webhooks'] ) &&
$vcs_config['use_webhooks']
)
) {
return;
}
$meta = upserv_get_package_metadata( $slug );
$type = isset( $meta['type'] ) ? $meta['type'] : null;
$hook = 'upserv_check_remote_' . $slug;
$params = array( $slug, $type, false );
if ( Scheduler::get_instance()->has_scheduled_action( $hook, $params ) ) {
return;
}
/**
* Filter the package update remote check frequency set in the configuration.
* Fired during client update API request.
*
* @param string $frequency The frequency set in the configuration.
* @param string $package_slug The slug of the package to check for updates.
* @since 1.0.0
*/
$frequency = apply_filters(
'upserv_check_remote_frequency',
$vcs_config['check_frequency'],
$slug
);
$timestamp = time();
$schedules = wp_get_schedules();
$result = Scheduler::get_instance()->schedule_recurring_action(
$timestamp,
$schedules[ $frequency ]['interval'],
$hook,
$params
);
/**
* Fired after a remote check event has been scheduled for a package.
* Fired during client update API request.
*
* @param bool $result Whether the event was scheduled.
* @param string $package_slug Slug of the package for which the event was scheduled.
* @param int $timestamp Timestamp for when to run the event the first time after it's been scheduled.
* @param string $frequency Frequency at which the event would be ran.
* @param string $hook Event hook to fire when the event is ran.
* @param array $params Parameters passed to the actions registered to $hook when the event is ran.
* @since 1.0.0
*/
do_action(
'upserv_scheduled_check_remote_event',
$result,
$slug,
$timestamp,
$frequency,
$hook,
$params
);
}
/**
* Handle API requests
*
* Process and respond to Update API requests.
*
* @since 1.0.0
*/
protected function handle_api_request() {
global $wp;
$vars = $wp->query_vars;
$params = array(
'action' => isset( $vars['action'] ) ? trim( $vars['action'] ) : null,
'token' => isset( $vars['token'] ) ? trim( $vars['token'] ) : null,
'slug' => isset( $vars['package_id'] ) ? trim( $vars['package_id'] ) : null,
'type' => isset( $vars['update_type'] ) ? trim( $vars['update_type'] ) : null,
);
$query = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$query = array_map(
'sanitize_text_field',
array_filter(
$query,
function ( $key ) use ( $query ) {
return (
! empty( $query[ $key ] ) &&
is_scalar( $query[ $key ] ) &&
preg_match( '@^[a-z0-9\-_]+$@i', $key )
);
},
ARRAY_FILTER_USE_KEY
)
);
/**
* Filter the parameters used to handle the request made by a client plugin, theme, or generic package to the plugin's API.
* Fired during client update API request.
*
* @param array $params The parameters of the request to the API.
* @since 1.0.0
*/
$params = apply_filters( 'upserv_handle_update_request_params', array_merge( $query, $params ) );
$this->init_server( $params['slug'] );
if ( ! $this->update_server ) {
wp_send_json(
array(
'error' => 'no_server',
'message' => __( 'No server found for this package.', 'updatepulse-server' ),
),
500,
Utils::JSON_OPTIONS
);
}
/**
* Fired before handling the request made by a client plugin, theme, or generic package to the plugin's API.
* Fired during client update API request.
*
* @param array $request_params The parameters or the request to the API.
* @since 1.0.0
*/
do_action( 'upserv_before_handle_update_request', $params );
$this->update_server->handle_request( $params );
}
/**
* Initialize update server
*
* Set up the update server for a specific package.
*
* @param string $slug The package slug.
* @since 1.0.0
*/
protected function init_server( $slug ) {
$check_manual = false;
if ( upserv_get_option( 'use_vcs' ) ) {
$vcs_config = upserv_get_package_vcs_config( $slug );
$url = isset( $vcs_config['url'] ) ? $vcs_config['url'] : false;
$branch = isset( $vcs_config['branch'] ) ? $vcs_config['branch'] : false;
$credentials = isset( $vcs_config['credentials'] ) ? $vcs_config['credentials'] : '';
$vcs_type = isset( $vcs_config['type'] ) ? $vcs_config['type'] : false;
$self_hosted = isset( $vcs_config['self_hosted'] ) ? $vcs_config['self_hosted'] : false;
if ( ! $url || ! $branch || ! $vcs_type ) {
$check_manual = true;
}
} else {
$check_manual = true;
}
if ( $check_manual ) {
$meta = upserv_get_package_metadata( $slug );
if ( ! isset( $meta['origin'] ) || 'manual' !== $meta['origin'] ) {
return;
}
}
$filter_args = array(
'url' => isset( $url ) ? $url : null,
'branch' => isset( $branch ) ? $branch : null,
'credentials' => isset( $credentials ) ? $credentials : null,
'self_hosted' => isset( $self_hosted ) ? $self_hosted : null,
'directory' => Data_Manager::get_data_dir(),
'vcs_config' => isset( $vcs_config ) ? $vcs_config : null,
);
/**
* Filter the class name to use to instantiate a `Anyape\UpdatePulse\Server\Server\Update\Update_Server` object.
* Fired during client update API request.
*
* @param string $class_name The class name to use to instantiate a `Anyape\UpdatePulse\Server\Server\Update\Update_Server` object.
* @param string $package_slug The slug of the package to serve.
* @param array $config The configuration to use to serve the package.
* @since 1.0.0
*/
$_class_name = apply_filters(
'upserv_server_class_name',
str_replace( 'API', 'Server\\Update', __NAMESPACE__ ) . '\\Update_Server',
$slug,
$filter_args
);
/**
* Filter the arguments to pass to the constructor of the `Anyape\UpdatePulse\Server\Server\Update\Update_Server` object.
* Fired during client update API request.
*
* @param array $args The arguments to pass to the constructor of the `Anyape\UpdatePulse\Server\Server\Update\Update_Server` object.
* @param string $package_slug The slug of the package to serve.
* @param array $config The configuration to use to serve the package.
* @since 1.0.0
*/
$args = apply_filters(
'upserv_server_constructor_args',
array(
home_url( '/updatepulse-server-update-api/' ),
Data_Manager::get_data_dir(),
isset( $url ) ? $url : null,
isset( $branch ) ? $branch : null,
isset( $credentials ) ? $credentials : null,
isset( $vcs_type ) ? $vcs_type : null,
isset( $self_hosted ) ? $self_hosted : null,
),
$slug,
$filter_args
);
if ( ! isset( $this->update_server ) || ! is_a( $this->update_server, $_class_name ) ) {
$this->update_server = new $_class_name( ...$args );
}
}
}