mirror of
https://gh.wpcy.net/https://github.com/Anyape/updatepulse-server.git
synced 2026-05-06 14:42:26 +08:00
1329 lines
38 KiB
PHP
1329 lines
38 KiB
PHP
<?php
|
|
|
|
namespace Anyape\UpdatePulse\Server\API;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit; // Exit if accessed directly
|
|
}
|
|
|
|
use WP_Error;
|
|
use Anyape\UpdatePulse\Server\Manager\Zip_Package_Manager;
|
|
use Anyape\UpdatePulse\Server\Manager\Data_Manager;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Cache;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Package;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Invalid_Package_Exception;
|
|
use Anyape\Utils\Utils;
|
|
|
|
/**
|
|
* Package API class
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class Package_API {
|
|
|
|
/**
|
|
* Is doing API request
|
|
*
|
|
* @var bool|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected static $doing_api_request = null;
|
|
/**
|
|
* Instance
|
|
*
|
|
* @var Package_API|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected static $instance;
|
|
/**
|
|
* Config
|
|
*
|
|
* @var array|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected static $config;
|
|
|
|
/**
|
|
* HTTP response code
|
|
*
|
|
* @var int|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $http_response_code = 200;
|
|
/**
|
|
* API key ID
|
|
*
|
|
* @var string|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $api_key_id;
|
|
/**
|
|
* API access
|
|
*
|
|
* @var array|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $api_access;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param boolean $init_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_saved_remote_package_to_local', array( $this, 'upserv_saved_remote_package_to_local' ), 10, 3 );
|
|
add_action( 'upserv_pre_delete_package', array( $this, 'upserv_pre_delete_package' ), 0, 2 );
|
|
add_action( 'upserv_did_delete_package', array( $this, 'upserv_did_delete_package' ), 20, 3 );
|
|
add_action( 'upserv_did_download_package', array( $this, 'upserv_did_download_package' ), 20, 1 );
|
|
|
|
add_filter( 'query_vars', array( $this, 'query_vars' ), -99, 1 );
|
|
add_filter( 'upserv_api_package_actions', array( $this, 'upserv_api_package_actions' ), 0, 1 );
|
|
add_filter( 'upserv_api_webhook_events', array( $this, 'upserv_api_webhook_events' ), 10, 1 );
|
|
add_filter( 'upserv_nonce_api_payload', array( $this, 'upserv_nonce_api_payload' ), 0, 1 );
|
|
add_filter( 'upserv_package_info_include', array( $this, 'upserv_package_info_include' ), 10, 2 );
|
|
}
|
|
}
|
|
|
|
/*******************************************************************
|
|
* Public methods
|
|
*******************************************************************/
|
|
|
|
// API action --------------------------------------------------
|
|
|
|
/**
|
|
* Browse packages
|
|
*
|
|
* Get information about multiple packages.
|
|
*
|
|
* @param string|array $query The search query or parameters.
|
|
* @return object Response with package information.
|
|
* @since 1.0.0
|
|
*/
|
|
public function browse( $query ) {
|
|
$result = false;
|
|
$query = empty( $query ) || ! is_string( $query ) ? array() : json_decode( wp_unslash( $query ), true );
|
|
$query['search'] = isset( $query['search'] ) ? trim( esc_html( $query['search'] ) ) : false;
|
|
$result = upserv_get_batch_package_info( $query['search'], false );
|
|
$result['count'] = is_array( $result ) ? count( $result ) : 0;
|
|
/**
|
|
* Filter the result of the `browse` operation of the Package API.
|
|
*
|
|
* @param array $result The result of the `browse` operation
|
|
* @param array $query The query - see browse()
|
|
* @return array The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters( 'upserv_package_browse', $result, $query );
|
|
|
|
/**
|
|
* Fired after the `browse` Package API action.
|
|
*
|
|
* @param array $result the result of the action
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_browse_package', $result );
|
|
|
|
if ( empty( $result ) ) {
|
|
$result = array( 'count' => 0 );
|
|
}
|
|
|
|
if ( isset( $result['count'] ) && 0 === $result['count'] ) {
|
|
$this->http_response_code = 404;
|
|
$result = (object) array(
|
|
'code' => 'no_packages_found',
|
|
'message' => __( 'No packages found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
/**
|
|
* Read package information
|
|
*
|
|
* Get information about a single package.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return object Response with package information.
|
|
* @since 1.0.0
|
|
*/
|
|
public function read( $package_id, $type ) {
|
|
$result = upserv_get_package_info( $package_id, false );
|
|
|
|
if (
|
|
! is_array( $result ) ||
|
|
! isset( $result['type'] ) ||
|
|
$type !== $result['type']
|
|
) {
|
|
$result = false;
|
|
} else {
|
|
unset( $result['file_path'] );
|
|
}
|
|
|
|
/**
|
|
* Filter the result of the `read` operation of the Package API.
|
|
*
|
|
* @param array $result The result of the `read` operation
|
|
* @param string $package_id The slug of the read package
|
|
* @param string $type The type of the read package
|
|
* @return array The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters( 'upserv_package_read', $result, $package_id, $type );
|
|
|
|
/**
|
|
* Fired after the `read` Package API action.
|
|
*
|
|
* @param array $result the result of the action
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_read_package', $result );
|
|
|
|
if ( ! $result ) {
|
|
$this->http_response_code = 404;
|
|
$result = (object) array(
|
|
'code' => 'package_not_found',
|
|
'message' => __( 'Package not found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
/**
|
|
* Edit a package
|
|
*
|
|
* If a package exists, update it by uploading a valid package file, or by downloading it if using a VCS.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return object Response with package information or error.
|
|
* @since 1.0.0
|
|
*/
|
|
public function edit( $package_id, $type ) {
|
|
$result = false;
|
|
$config = self::get_config();
|
|
$exists = upserv_get_package_info( $package_id, false );
|
|
|
|
if ( ! empty( $exists ) ) {
|
|
$file = $this->get_file();
|
|
|
|
if ( $file ) {
|
|
$result = $this->process_file( $file, $package_id, $type );
|
|
} elseif ( $config['use_vcs'] ) {
|
|
$result = $this->download_file( $package_id, $type );
|
|
}
|
|
|
|
$result = $result && ! is_wp_error( $result ) ? upserv_get_package_info( $package_id, false ) : $result;
|
|
}
|
|
|
|
/**
|
|
* Filter the result of the `edit` operation of the Package API.
|
|
*
|
|
* @param array $result The result of the `edit` operation
|
|
* @param string $package_id The slug of the edited package
|
|
* @param string $type The type of the edited package
|
|
* @return array The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters( 'upserv_package_edit', $result, $package_id, $type );
|
|
|
|
if ( empty( $exists ) ) {
|
|
$this->http_response_code = 404;
|
|
$result = (object) array(
|
|
'code' => 'package_not_found',
|
|
'message' => __( 'Package not found.', 'updatepulse-server' ),
|
|
);
|
|
} elseif ( is_wp_error( $result ) ) {
|
|
$this->http_response_code = 400;
|
|
$result = (object) array(
|
|
'code' => $result->get_error_code(),
|
|
'message' => $result->get_error_message(),
|
|
);
|
|
} elseif ( ! $result ) {
|
|
$this->http_response_code = 400;
|
|
$result = (object) array(
|
|
'code' => 'invalid_parameters',
|
|
'message' => __( 'Package could not be edited - invalid parameters.', 'updatepulse-server' ),
|
|
);
|
|
} else {
|
|
/**
|
|
* Fired after the `edit` Package API action.
|
|
*
|
|
* @param array $result the result of the action
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_edit_package', $result );
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
/**
|
|
* Add a package
|
|
*
|
|
* If a package does not exist, upload it by providing a valid package file, or download it if using a VCS.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return object Response with package information or error.
|
|
* @since 1.0.0
|
|
*/
|
|
public function add( $package_id, $type ) {
|
|
$result = false;
|
|
$config = self::get_config();
|
|
$exists = upserv_get_package_info( $package_id, false );
|
|
|
|
if ( empty( $exists ) ) {
|
|
$file = $this->get_file();
|
|
|
|
if ( $file ) {
|
|
$result = $this->process_file( $file, $package_id, $type );
|
|
} elseif ( $config['use_vcs'] ) {
|
|
$result = $this->download_file( $package_id, $type );
|
|
}
|
|
|
|
$result = $result && ! is_wp_error( $result ) ? upserv_get_package_info( $package_id, false ) : $result;
|
|
}
|
|
|
|
/**
|
|
* Filter the result of the `add` operation of the Package API.
|
|
*
|
|
* @param array $result The result of the `add` operation
|
|
* @param string $package_id The slug of the added package
|
|
* @param string $type The type of the added package
|
|
* @return array The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters( 'upserv_package_add', $result, $package_id, $type );
|
|
|
|
if ( ! empty( $exists ) ) {
|
|
$this->http_response_code = 409;
|
|
$result = (object) array(
|
|
'code' => 'package_exists',
|
|
'message' => __( 'Package already exists.', 'updatepulse-server' ),
|
|
);
|
|
} elseif ( is_wp_error( $result ) ) {
|
|
$this->http_response_code = 400;
|
|
$result = (object) array(
|
|
'code' => $result->get_error_code(),
|
|
'message' => $result->get_error_message(),
|
|
);
|
|
} elseif ( ! $result ) {
|
|
$this->http_response_code = 400;
|
|
$result = (object) array(
|
|
'code' => 'invalid_parameters',
|
|
'message' => __( 'Package could not be added - invalid parameters.', 'updatepulse-server' ),
|
|
);
|
|
} else {
|
|
/**
|
|
* Fired after the `add` Package API action.
|
|
*
|
|
* @param array $result the result of the action
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_add_package', $result );
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
/**
|
|
* Delete a package
|
|
*
|
|
* Remove a package from the system.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return object Response with deletion status or error.
|
|
* @since 1.0.0
|
|
*/
|
|
public function delete( $package_id, $type ) {
|
|
/**
|
|
* Fired before the `delete` Package API action.
|
|
*
|
|
* @param string $package_slug the slug of the package to be deleted
|
|
* @param string $type the type of the package to be deleted
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_pre_delete_package', $package_id, $type );
|
|
|
|
$result = upserv_delete_package( $package_id );
|
|
/**
|
|
* Filter the result of the `delete` operation of the Package API.
|
|
*
|
|
* @param bool $result The result of the `delete` operation
|
|
* @param string $package_id The slug of the deleted package
|
|
* @param string $type The type of the deleted package
|
|
* @return bool The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters( 'upserv_package_delete', $result, $package_id, $type );
|
|
|
|
if ( $result ) {
|
|
/**
|
|
* Fired after the `delete` Package API action.
|
|
*
|
|
* @param bool $result the result of the `delete` operation
|
|
* @param string $package_slug the slug of the deleted package
|
|
* @param string $type the type of the deleted package
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_delete_package', $result, $package_id, $type );
|
|
} else {
|
|
$this->http_response_code = 404;
|
|
$result = (object) array(
|
|
'code' => 'package_not_found',
|
|
'message' => __( 'Package not found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
/**
|
|
* Download a package
|
|
*
|
|
* Initiate download of a package file.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return array Error information if package not found.
|
|
* @since 1.0.0
|
|
*/
|
|
public function download( $package_id, $type ) {
|
|
$path = upserv_get_local_package_path( $package_id );
|
|
|
|
if ( ! $path ) {
|
|
|
|
if ( ! $this->add( $package_id, $type ) ) {
|
|
return array(
|
|
'code' => 'package_not_found',
|
|
'message' => __( 'Package not found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
}
|
|
|
|
upserv_download_local_package( $package_id, $path, false );
|
|
/**
|
|
* Fired after the `download` Package API action.
|
|
*
|
|
* @param string $package_slug the slug of the downloaded package
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_download_package', $package_id );
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Generate signed URL for package download
|
|
*
|
|
* Create a secure URL for downloading packages.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return object Response with signed URL information.
|
|
* @since 1.0.0
|
|
*/
|
|
public function signed_url( $package_id, $type ) {
|
|
$package_id = filter_var( $package_id, FILTER_SANITIZE_URL );
|
|
$type = filter_var( $type, FILTER_SANITIZE_URL );
|
|
/**
|
|
* Filter the token used to sign the URL.
|
|
*
|
|
* @param mixed $token The token used to sign the URL
|
|
* @param string $package_id The slug of the package for which the URL needs to be signed
|
|
* @param string $type The type of the package for which the URL needs to be signed
|
|
* @return mixed The filtered token
|
|
* @since 1.0.0
|
|
*/
|
|
$token = apply_filters( 'upserv_package_signed_url_token', false, $package_id, $type );
|
|
|
|
if ( ! $token ) {
|
|
$token = upserv_create_nonce(
|
|
false,
|
|
HOUR_IN_SECONDS,
|
|
array(
|
|
'actions' => array( 'download' ),
|
|
'type' => $type,
|
|
'package_id' => $package_id,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filter the result of the `signed_url` operation of the Package API.
|
|
*
|
|
* @param array $result The result of the `signed_url` operation
|
|
* @param string $package_id The slug of the package for which the URL was signed
|
|
* @param string $type The type of the package for which the URL was signed
|
|
* @return array The filtered result
|
|
* @since 1.0.0
|
|
*/
|
|
$result = apply_filters(
|
|
'upserv_package_signed_url',
|
|
array(
|
|
'url' => add_query_arg(
|
|
array(
|
|
'token' => $token,
|
|
'action' => 'download',
|
|
),
|
|
home_url( 'updatepulse-server-package-api/' . $type . '/' . $package_id . '/' )
|
|
),
|
|
'token' => $token,
|
|
'expiry' => upserv_get_nonce_expiry( $token ),
|
|
),
|
|
$package_id,
|
|
$type
|
|
);
|
|
|
|
if ( $result ) {
|
|
/**
|
|
* Fired after the `signed_url` Package API action.
|
|
*
|
|
* @param array $result the result of the action
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_did_signed_url_package', $result );
|
|
} else {
|
|
$this->http_response_code = 404;
|
|
$result = (object) array(
|
|
'code' => 'package_not_found',
|
|
'message' => __( 'Package not found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
|
|
return (object) $result;
|
|
}
|
|
|
|
// WordPress hooks ---------------------------------------------
|
|
|
|
/**
|
|
* Add API endpoints
|
|
*
|
|
* Register the rewrite rules for the Package API endpoints.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public function add_endpoints() {
|
|
add_rewrite_rule(
|
|
'^updatepulse-server-package-api/(plugin|theme|generic)/(.+)/*?$',
|
|
'index.php?type=$matches[1]&package_id=$matches[2]&$matches[3]&__upserv_package_api=1&',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^updatepulse-server-package-api/*?$',
|
|
'index.php?$matches[1]&__upserv_package_api=1&',
|
|
'top'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse API requests
|
|
*
|
|
* Handle incoming API requests to the Package API endpoints.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public function parse_request() {
|
|
global $wp;
|
|
|
|
if ( isset( $wp->query_vars['__upserv_package_api'] ) ) {
|
|
$this->handle_api_request();
|
|
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register query variables
|
|
*
|
|
* Add custom query variables used by the Package 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_package_api',
|
|
'action',
|
|
'api',
|
|
'api_token',
|
|
'api_credentials',
|
|
'package_id',
|
|
'type',
|
|
'browse_query',
|
|
)
|
|
);
|
|
|
|
return $query_vars;
|
|
}
|
|
|
|
/**
|
|
* Handle package saved to local event
|
|
*
|
|
* Actions to perform when a remote package has been saved locally.
|
|
*
|
|
* @param bool $local_ready Whether the local package is ready.
|
|
* @param string $package_type The type of the package.
|
|
* @param string $package_slug The slug of the package.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_saved_remote_package_to_local( $local_ready, $package_type, $package_slug ) {
|
|
|
|
if ( ! $local_ready ) {
|
|
return;
|
|
}
|
|
|
|
upserv_whitelist_package( $package_slug );
|
|
|
|
$payload = array(
|
|
'event' => 'package_updated',
|
|
// translators: %1$s is the package type, %2$s is the pakage slug
|
|
'description' => sprintf( esc_html__( 'The package of type `%1$s` and slug `%2$s` has been updated on UpdatePulse Server', 'updatepulse-server' ), $package_type, $package_slug ),
|
|
'content' => upserv_get_package_info( $package_slug, false ),
|
|
);
|
|
|
|
upserv_schedule_webhook( $payload, 'package' );
|
|
}
|
|
|
|
/**
|
|
* Handle pre-delete package event
|
|
*
|
|
* Actions to perform before a package is deleted.
|
|
*
|
|
* @param string $package_slug The slug of the package.
|
|
* @param string $package_type The type of the package.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_pre_delete_package( $package_slug, $package_type ) {
|
|
wp_cache_set(
|
|
'upserv_package_deleted_info' . $package_slug . '_' . $package_type,
|
|
upserv_get_package_info( $package_slug, false ),
|
|
'updatepulse-server'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle post-delete package event
|
|
*
|
|
* Actions to perform after a package is deleted.
|
|
*
|
|
* @param bool $result The result of the deletion.
|
|
* @param string $package_slug The slug of the package.
|
|
* @param string $package_type The type of the package.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_did_delete_package( $result, $package_slug, $package_type ) {
|
|
$package_info = wp_cache_get(
|
|
'upserv_package_deleted_info' . $package_slug . '_' . $package_type,
|
|
'updatepulse-server'
|
|
);
|
|
|
|
if ( $package_info ) {
|
|
$payload = array(
|
|
'event' => 'package_deleted',
|
|
// translators: %1$s is the package type, %2$s is the package slug
|
|
'description' => sprintf( esc_html__( 'The package of type `%1$s` and slug `%2$s` has been deleted on UpdatePulse Server', 'updatepulse-server' ), $package_type, $package_slug ),
|
|
'content' => $package_info,
|
|
);
|
|
|
|
upserv_schedule_webhook( $payload, 'package' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle package downloaded event
|
|
*
|
|
* Actions to perform after a package is downloaded.
|
|
*
|
|
* @param string $package_slug The slug of the downloaded package.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_did_download_package( $package_slug ) {
|
|
$payload = array(
|
|
'event' => 'package_downloaded',
|
|
// translators: %s is the package slug
|
|
'description' => sprintf( esc_html__( 'The package of `%s` has been securely downloaded from UpdatePulse Server', 'updatepulse-server' ), $package_slug ),
|
|
'content' => upserv_get_package_info( $package_slug, false ),
|
|
);
|
|
|
|
upserv_schedule_webhook( $payload, 'package' );
|
|
}
|
|
|
|
/**
|
|
* Register package API actions
|
|
*
|
|
* Add descriptions for all available Package API actions.
|
|
*
|
|
* @param array $actions Existing API actions.
|
|
* @return array Modified API actions with descriptions.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_api_package_actions( $actions ) {
|
|
$actions['browse'] = __( 'Get information about multiple packages', 'updatepulse-server' );
|
|
$actions['read'] = __( 'Get information about a single package', 'updatepulse-server' );
|
|
$actions['edit'] = __( 'If a package does exist, update it by uploading a valid package file, or by downloading it if using a VCS', 'updatepulse-server' );
|
|
$actions['add'] = __( 'If a package does not exist, upload it by providing a valid package file, or download it if using a VCS', 'updatepulse-server' );
|
|
$actions['delete'] = __( 'Delete a package', 'updatepulse-server' );
|
|
$actions['signed_url'] = __( 'Retrieve secure URLs for downloading packages', 'updatepulse-server' );
|
|
|
|
return $actions;
|
|
}
|
|
|
|
/**
|
|
* Register webhook events
|
|
*
|
|
* Add supported webhook events for the Package API.
|
|
*
|
|
* @param array $webhook_events Existing webhook events.
|
|
* @return array Modified webhook events.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_api_webhook_events( $webhook_events ) {
|
|
|
|
if ( isset( $webhook_events['package'], $webhook_events['package']['events'] ) ) {
|
|
$webhook_events['package']['events']['package_updated'] = __( 'Package added or updated', 'updatepulse-server' );
|
|
$webhook_events['package']['events']['package_deleted'] = __( 'Package deleted', 'updatepulse-server' );
|
|
$webhook_events['package']['events']['package_downloaded'] = __( 'Package downloaded via a signed URL', 'updatepulse-server' );
|
|
}
|
|
|
|
return $webhook_events;
|
|
}
|
|
|
|
/**
|
|
* Fetch nonce for public API
|
|
*
|
|
* Validate nonce for public API requests.
|
|
*
|
|
* @param mixed $nonce The nonce to validate.
|
|
* @param mixed $true_nonce The true nonce value.
|
|
* @param int $expiry The nonce expiry time.
|
|
* @param array $data Additional data associated with the nonce.
|
|
* @return mixed Validated nonce or null if invalid.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_fetch_nonce_public( $nonce, $true_nonce, $expiry, $data ) {
|
|
global $wp;
|
|
|
|
$current_action = $wp->query_vars['action'];
|
|
|
|
if (
|
|
isset( $data['actions'] ) &&
|
|
is_array( $data['actions'] ) &&
|
|
! empty( $data['actions'] )
|
|
) {
|
|
|
|
if ( ! in_array( $current_action, $data['actions'], true ) ) {
|
|
$nonce = null;
|
|
} elseif ( isset( $data['type'], $data['package_id'] ) ) {
|
|
$type = isset( $wp->query_vars['type'] ) ? $wp->query_vars['type'] : null;
|
|
$package_id = isset( $wp->query_vars['package_id'] ) ? $wp->query_vars['package_id'] : null;
|
|
|
|
if ( $type !== $data['type'] || $package_id !== $data['package_id'] ) {
|
|
$nonce = null;
|
|
}
|
|
}
|
|
} else {
|
|
$nonce = null;
|
|
}
|
|
|
|
return $nonce;
|
|
}
|
|
|
|
/**
|
|
* Fetch nonce for private API
|
|
*
|
|
* Validate nonce for private API requests.
|
|
*
|
|
* @param mixed $nonce The nonce to validate.
|
|
* @param mixed $true_nonce The true nonce value.
|
|
* @param int $expiry The nonce expiry time.
|
|
* @param array $data Additional data associated with the nonce.
|
|
* @return mixed Validated nonce or null if invalid.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_fetch_nonce_private( $nonce, $true_nonce, $expiry, $data ) {
|
|
$config = self::get_config();
|
|
$valid = false;
|
|
|
|
if (
|
|
! empty( $config['private_api_auth_keys'] ) &&
|
|
isset( $data['package_api'], $data['package_api']['id'], $data['package_api']['access'] )
|
|
) {
|
|
global $wp;
|
|
|
|
$action = $wp->query_vars['action'];
|
|
|
|
foreach ( $config['private_api_auth_keys'] as $id => $values ) {
|
|
|
|
if (
|
|
$id === $data['package_api']['id'] &&
|
|
isset( $values['access'] ) &&
|
|
is_array( $values['access'] ) &&
|
|
(
|
|
in_array( 'all', $values['access'], true ) ||
|
|
in_array( $action, $values['access'], true )
|
|
)
|
|
) {
|
|
$this->api_key_id = $id;
|
|
$this->api_access = $values['access'];
|
|
$valid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! $valid ) {
|
|
$nonce = null;
|
|
}
|
|
|
|
return $nonce;
|
|
}
|
|
|
|
/**
|
|
* Modify nonce API payload
|
|
*
|
|
* Adjust the payload for API nonce creation.
|
|
*
|
|
* @param array $payload The original payload.
|
|
* @return array Modified payload.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_nonce_api_payload( $payload ) {
|
|
global $wp;
|
|
|
|
if ( ! isset( $wp->query_vars['api'] ) || 'package' !== $wp->query_vars['api'] ) {
|
|
return $payload;
|
|
}
|
|
|
|
$key_id = false;
|
|
$credentials = array();
|
|
$config = self::get_config();
|
|
|
|
if ( ! empty( $_SERVER['HTTP_X_UPDATEPULSE_API_CREDENTIALS'] ) ) {
|
|
$credentials = explode(
|
|
'|',
|
|
sanitize_text_field(
|
|
wp_unslash( $_SERVER['HTTP_X_UPDATEPULSE_API_CREDENTIALS'] )
|
|
)
|
|
);
|
|
} elseif (
|
|
isset( $wp->query_vars['api_credentials'], $wp->query_vars['api'] ) &&
|
|
is_string( $wp->query_vars['api_credentials'] ) &&
|
|
! empty( $wp->query_vars['api_credentials'] )
|
|
) {
|
|
$credentials = explode( '|', $wp->query_vars['api_credentials'] );
|
|
}
|
|
|
|
if ( 2 === count( $credentials ) ) {
|
|
$key_id = end( $credentials );
|
|
}
|
|
|
|
if ( $key_id && isset( $config['private_api_auth_keys'][ $key_id ]['key'] ) ) {
|
|
$values = $config['private_api_auth_keys'][ $key_id ];
|
|
$payload['data']['package_api'] = array(
|
|
'id' => $key_id,
|
|
'access' => isset( $values['access'] ) ? $values['access'] : array(),
|
|
);
|
|
}
|
|
|
|
$payload['expiry_length'] = HOUR_IN_SECONDS / 2;
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Filter package information inclusion
|
|
*
|
|
* Determine whether to include package information in responses.
|
|
*
|
|
* @param bool $_include Current inclusion status.
|
|
* @param array $info Package information.
|
|
* @return bool Whether to include the package information.
|
|
* @since 1.0.0
|
|
*/
|
|
public function upserv_package_info_include( $_include, $info ) {
|
|
return ! upserv_get_option( 'use_vcs' ) || upserv_is_package_whitelisted( $info['slug'] );
|
|
}
|
|
|
|
// Misc. -------------------------------------------------------
|
|
|
|
/**
|
|
* Check if currently processing an API request
|
|
*
|
|
* Determine whether the current request is a Package API request.
|
|
*
|
|
* @return bool Whether the current request is a Package 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-package-api$/' );
|
|
}
|
|
|
|
return self::$doing_api_request;
|
|
}
|
|
|
|
/**
|
|
* Get Package API configuration
|
|
*
|
|
* Retrieve and filter the Package API configuration settings.
|
|
*
|
|
* @return array Package API configuration.
|
|
* @since 1.0.0
|
|
*/
|
|
public static function get_config() {
|
|
|
|
if ( ! self::$config ) {
|
|
$config = array(
|
|
'use_vcs' => upserv_get_option( 'use_vcs' ),
|
|
'private_api_auth_keys' => upserv_get_option( 'api/packages/private_api_keys' ),
|
|
'ip_whitelist' => upserv_get_option( 'api/packages/private_api_ip_whitelist' ),
|
|
);
|
|
|
|
self::$config = $config;
|
|
}
|
|
|
|
/**
|
|
* Filter the configuration of the Package API.
|
|
*
|
|
* @param array $config The configuration of the Package API
|
|
* @return array The filtered configuration
|
|
* @since 1.0.0
|
|
*/
|
|
return apply_filters( 'upserv_package_api_config', self::$config );
|
|
}
|
|
|
|
/**
|
|
* Get Package API instance
|
|
*
|
|
* Retrieve or create the Package API singleton instance.
|
|
*
|
|
* @return Package_API The Package API instance.
|
|
* @since 1.0.0
|
|
*/
|
|
public static function get_instance() {
|
|
|
|
if ( ! self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/*******************************************************************
|
|
* Protected methods
|
|
*******************************************************************/
|
|
|
|
/**
|
|
* Get uploaded file
|
|
*
|
|
* Retrieve the uploaded file from a request.
|
|
*
|
|
* @return array|false File information array or false if no valid file.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function get_file() {
|
|
$files = $_FILES; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
|
$return = false;
|
|
|
|
if (
|
|
is_array( $files ) &&
|
|
isset( $files['file'], $files['file']['tmp_name'], $files['file']['name'] ) &&
|
|
sanitize_file_name( $files['file']['tmp_name'] ) === $files['file']['tmp_name'] &&
|
|
sanitize_file_name( $files['file']['name'] ) === $files['file']['name']
|
|
) {
|
|
$return = array( $files['file']['tmp_name'], $files['file']['name'] );
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Process uploaded package file
|
|
*
|
|
* Handle validation and processing of an uploaded package file.
|
|
*
|
|
* @param array $file The file information array.
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return bool|WP_Error True on success, WP_Error on failure.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function process_file( $file, $package_id, $type ) {
|
|
list(
|
|
$local_filename,
|
|
$filename
|
|
) = $file;
|
|
$file_hash = ! empty( $_SERVER['HTTP_FILE_HASH'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_FILE_HASH'] ) ) : false;
|
|
|
|
if ( hash_file( 'sha256', $local_filename ) !== $file_hash ) {
|
|
wp_delete_file( $local_filename );
|
|
|
|
return new WP_Error(
|
|
'invalid_hash',
|
|
__( 'The provided file does not match the provided hash.', 'updatepulse-server' )
|
|
);
|
|
}
|
|
|
|
$zip_check = wp_check_filetype( $filename, array( 'zip' => 'application/zip' ) );
|
|
$bytes = filesize( $local_filename ) > 4 ?
|
|
file_get_contents( $local_filename, false, null, 0, 4 ) : // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
|
false;
|
|
$bytes_check = $bytes ? '504b0304' === bin2hex( $bytes ) : false;
|
|
|
|
if (
|
|
! $bytes_check ||
|
|
'zip' !== $zip_check['ext'] ||
|
|
'application/zip' !== mime_content_type( $local_filename )
|
|
) {
|
|
wp_delete_file( $local_filename );
|
|
|
|
return new WP_Error(
|
|
'invalid_file_type',
|
|
__( 'The provided file is not a valid ZIP file.', 'updatepulse-server' )
|
|
);
|
|
}
|
|
|
|
$package_manager = new Zip_Package_Manager(
|
|
$package_id,
|
|
$local_filename,
|
|
Data_Manager::get_data_dir( 'tmp' ),
|
|
Data_Manager::get_data_dir( 'packages' )
|
|
);
|
|
|
|
$package = null;
|
|
$result = false;
|
|
|
|
try {
|
|
$result = $package_manager->clean_package();
|
|
$cache = new Cache( Data_Manager::get_data_dir( 'cache' ) );
|
|
$file_path = Data_Manager::get_data_dir( 'packages' ) . $package_id . '.zip';
|
|
$package = Package::from_archive( $file_path, $package_id, $cache );
|
|
} catch ( Invalid_Package_Exception ) {
|
|
wp_delete_file( $local_filename );
|
|
wp_delete_file( Data_Manager::get_data_dir( 'tmp' ) . $package_id . '.zip' );
|
|
wp_delete_file( Data_Manager::get_data_dir( 'packages' ) . $package_id . '.zip' );
|
|
|
|
$result = false;
|
|
}
|
|
|
|
if ( ! $result ) {
|
|
return new WP_Error(
|
|
'invalid_package',
|
|
__( 'The provided file is not a valid package.', 'updatepulse-server' )
|
|
);
|
|
}
|
|
|
|
$vcs_url = filter_input( INPUT_POST, 'vcs_url', FILTER_SANITIZE_URL );
|
|
$branch = sanitize_text_field( wp_unslash( filter_input( INPUT_POST, 'branch' ) ) );
|
|
$meta = upserv_get_package_metadata( $package_id );
|
|
$meta['type'] = $type;
|
|
|
|
if ( $vcs_url ) {
|
|
$branch = $branch ? $branch : 'main';
|
|
$vcs_configs = upserv_get_option( 'vcs', array() );
|
|
$meta['vcs_key'] = hash( 'sha256', trailingslashit( $vcs_url ) . '|' . $branch );
|
|
$meta['origin'] = 'vcs';
|
|
$meta['branch'] = $branch;
|
|
$meta['vcs'] = trailingslashit( $vcs_url );
|
|
|
|
if ( isset( $vcs_configs[ $meta['vcs_key'] ] ) ) {
|
|
upserv_set_package_metadata( $package_id, $meta );
|
|
} else {
|
|
wp_delete_file( $package->get_filename() );
|
|
|
|
return new WP_Error(
|
|
'invalid_vcs',
|
|
__( 'The provided VCS information is not valid', 'updatepulse-server' )
|
|
);
|
|
}
|
|
} else {
|
|
$meta['origin'] = 'manual';
|
|
|
|
upserv_set_package_metadata( $package_id, $meta );
|
|
}
|
|
|
|
/**
|
|
* Fired after an attempt to save a downloaded package on the file system has been performed.
|
|
* Fired during client update API request.
|
|
*
|
|
* @param bool $result `true` in case of success, `false` otherwise
|
|
* @param string $type type of the saved package - `"Plugin"`, `"Theme"`, or `"Generic"`
|
|
* @param string $package_slug slug of the saved package
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_saved_remote_package_to_local', true, $type, $package_id );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Download a package file from VCS
|
|
*
|
|
* Fetch a package from its version control system source.
|
|
*
|
|
* @param string $package_id The package ID/slug.
|
|
* @param string $type The package type.
|
|
* @return bool|WP_Error True on success, WP_Error on failure.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function download_file( $package_id, $type ) {
|
|
$vcs_url = filter_input( INPUT_POST, 'vcs_url', FILTER_SANITIZE_URL );
|
|
$branch = sanitize_text_field( wp_unslash( filter_input( INPUT_POST, 'branch' ) ) );
|
|
$result = false;
|
|
|
|
if ( $vcs_url ) {
|
|
$branch = $branch ? $branch : 'main';
|
|
$result = upserv_download_remote_package( $package_id, $type, $vcs_url, $branch );
|
|
} else {
|
|
$result = upserv_download_remote_package( $package_id, $type );
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Authorize public API request
|
|
*
|
|
* Validate authorization for public API endpoints.
|
|
*
|
|
* @return bool Whether the request is authorized.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function authorize_public() {
|
|
$nonce = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'token' ) ) );
|
|
|
|
if ( ! $nonce ) {
|
|
$nonce = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'nonce' ) ) );
|
|
}
|
|
|
|
add_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_public' ), 10, 4 );
|
|
|
|
$result = upserv_validate_nonce( $nonce );
|
|
|
|
remove_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_public' ), 10 );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Authorize private API request
|
|
*
|
|
* Validate authorization for private API endpoints.
|
|
*
|
|
* @param string $action The requested API action.
|
|
* @return bool Whether the request is authorized.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function authorize_private( $action ) {
|
|
$token = false;
|
|
$is_auth = false;
|
|
|
|
if ( ! empty( $_SERVER['HTTP_X_UPDATEPULSE_TOKEN'] ) ) {
|
|
$token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_UPDATEPULSE_TOKEN'] ) );
|
|
} else {
|
|
global $wp;
|
|
|
|
if (
|
|
isset( $wp->query_vars['api_token'] ) &&
|
|
is_string( $wp->query_vars['api_token'] ) &&
|
|
! empty( $wp->query_vars['api_token'] )
|
|
) {
|
|
$token = $wp->query_vars['api_token'];
|
|
}
|
|
}
|
|
|
|
add_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_private' ), 10, 4 );
|
|
|
|
$is_auth = upserv_validate_nonce( $token );
|
|
|
|
remove_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_private' ), 10 );
|
|
|
|
if ( $this->api_key_id && $this->api_access ) {
|
|
$is_auth = $is_auth && (
|
|
in_array( 'all', $this->api_access, true ) ||
|
|
in_array( $action, $this->api_access, true )
|
|
);
|
|
}
|
|
|
|
return $is_auth;
|
|
}
|
|
|
|
/**
|
|
* Check if API action is public
|
|
*
|
|
* Determine if a specific API action is available publicly.
|
|
*
|
|
* @param string $method The API method to check.
|
|
* @return bool Whether the API action is public.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function is_api_public( $method ) {
|
|
/**
|
|
* Filter the public API actions; public actions can be accessed via the `GET` method and a token,
|
|
* all other actions are considered private and can only be accessed via the `POST` method.
|
|
*
|
|
* @param array $public_api_actions The public API actions
|
|
* @return array The filtered public API actions
|
|
* @since 1.0.0
|
|
*/
|
|
$public_api = apply_filters(
|
|
'upserv_package_public_api_actions',
|
|
array( 'download' )
|
|
);
|
|
$is_api_public = in_array( $method, $public_api, true );
|
|
|
|
return $is_api_public;
|
|
}
|
|
|
|
/**
|
|
* Handle incoming API requests
|
|
*
|
|
* Process and respond to Package API requests.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
protected function handle_api_request() {
|
|
global $wp;
|
|
|
|
$method = isset( $wp->query_vars['action'] ) ? $wp->query_vars['action'] : false;
|
|
|
|
if (
|
|
sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'action' ) ) ) &&
|
|
! $this->is_api_public( $method )
|
|
) {
|
|
$this->http_response_code = 405;
|
|
$response = array(
|
|
'code' => 'method_not_allowed',
|
|
'message' => __( 'Unauthorized GET method.', 'updatepulse-server' ),
|
|
);
|
|
} else {
|
|
$malformed_request = false;
|
|
|
|
if ( ! isset( $wp->query_vars['action'] ) ) {
|
|
$malformed_request = true;
|
|
} elseif (
|
|
'browse' === $wp->query_vars['action'] &&
|
|
isset( $wp->query_vars['browse_query'] )
|
|
) {
|
|
$payload = $wp->query_vars['browse_query'];
|
|
} else {
|
|
$payload = $wp->query_vars;
|
|
}
|
|
|
|
if ( ! $malformed_request ) {
|
|
/**
|
|
* Filter whether the Package API request is authorized
|
|
*
|
|
* @param bool $authorized Whether the Package API request is authorized
|
|
* @param string $method The method of the request - `GET` or `POST`
|
|
* @param array $payload The payload of the request
|
|
* @return bool The filtered authorization status
|
|
* @since 1.0.0
|
|
*/
|
|
$authorized = apply_filters(
|
|
'upserv_package_api_request_authorized',
|
|
(
|
|
(
|
|
$this->is_api_public( $method ) &&
|
|
$this->authorize_public()
|
|
) ||
|
|
(
|
|
$this->authorize_private( $method ) &&
|
|
$this->authorize_ip()
|
|
)
|
|
),
|
|
$method,
|
|
$payload
|
|
);
|
|
|
|
if ( $authorized ) {
|
|
/**
|
|
* Fired before the Package API request is processed; useful to bypass the execution of currently implemented actions, or implement new actions.
|
|
*
|
|
* @param string $action the Package API action
|
|
* @param array $payload the payload of the request
|
|
* @since 1.0.0
|
|
*/
|
|
do_action( 'upserv_package_api_request', $method, $payload );
|
|
|
|
if ( method_exists( $this, $method ) ) {
|
|
$type = isset( $payload['type'] ) ? $payload['type'] : null;
|
|
$package_id = isset( $payload['package_id'] ) ? $payload['package_id'] : null;
|
|
|
|
if ( $type && $package_id ) {
|
|
$response = $this->$method( $package_id, $type );
|
|
} else {
|
|
$response = $this->$method( $payload );
|
|
}
|
|
|
|
if ( is_object( $response ) && ! empty( get_object_vars( $response ) ) ) {
|
|
$response->time_elapsed = Utils::get_time_elapsed();
|
|
}
|
|
} else {
|
|
$this->http_response_code = 400;
|
|
$response = array(
|
|
'code' => 'action_not_found',
|
|
'message' => __( 'Package API action not found.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
} else {
|
|
$this->http_response_code = 403;
|
|
$response = array(
|
|
'code' => 'unauthorized',
|
|
'message' => __( 'Unauthorized access.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
} else {
|
|
$this->http_response_code = 400;
|
|
$response = array(
|
|
'code' => 'malformed_request',
|
|
'message' => __( 'Malformed request.', 'updatepulse-server' ),
|
|
);
|
|
}
|
|
}
|
|
|
|
wp_send_json( $response, $this->http_response_code, Utils::JSON_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Authorize request by IP address
|
|
*
|
|
* Validate if the request IP is allowed.
|
|
*
|
|
* @return bool Whether the request IP is authorized.
|
|
* @since 1.0.0
|
|
*/
|
|
protected function authorize_ip() {
|
|
$result = false;
|
|
$config = self::get_config();
|
|
|
|
if ( is_array( $config['ip_whitelist'] ) && ! empty( $config['ip_whitelist'] ) ) {
|
|
|
|
foreach ( $config['ip_whitelist'] as $range ) {
|
|
|
|
if ( Utils::cidr_match( Utils::get_remote_ip(), $range ) ) {
|
|
$result = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$result = true;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|