mirror of
https://gh.wpcy.net/https://github.com/Anyape/updatepulse-server.git
synced 2026-05-05 22:26:46 +08:00
1714 lines
46 KiB
PHP
1714 lines
46 KiB
PHP
<?php
|
|
|
|
namespace Anyape\UpdatePulse\Server\Server\Update;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit; // Exit if accessed directly
|
|
}
|
|
|
|
use DateTime;
|
|
use DateTimeZone;
|
|
use WP_Error;
|
|
use Exception;
|
|
use Anyape\UpdatePulse\Package_Parser\Parser as Package_Parser;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Cache;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Package;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Request;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Headers;
|
|
use Anyape\UpdatePulse\Server\Server\Update\Invalid_Package_Exception;
|
|
use Anyape\UpdatePulse\Server\Manager\Data_Manager;
|
|
use Anyape\UpdatePulse\Server\Manager\Zip_Package_Manager;
|
|
use Anyape\UpdatePulse\Server\Server\License\License_Server;
|
|
use Anyape\Utils\Utils;
|
|
|
|
/**
|
|
* Update Server class
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class Update_Server {
|
|
|
|
/**
|
|
* Lock duration for remote updates in seconds
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
const LOCK_REMOTE_UPDATE_SEC = 10;
|
|
|
|
/**
|
|
* Directory for package storage
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $package_dir;
|
|
/**
|
|
* Directory for log storage
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $log_dir;
|
|
/**
|
|
* Cache instance
|
|
*
|
|
* @var Cache
|
|
* @since 1.0.0
|
|
*/
|
|
protected $cache;
|
|
/**
|
|
* Server URL
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $server_url;
|
|
/**
|
|
* Server timezone
|
|
*
|
|
* @var DateTimeZone
|
|
* @since 1.0.0
|
|
*/
|
|
protected $timezone;
|
|
/**
|
|
* Server directory
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $server_dir;
|
|
/**
|
|
* Version control system URL
|
|
*
|
|
* @var string|false
|
|
* @since 1.0.0
|
|
*/
|
|
protected $vcs_url;
|
|
/**
|
|
* Branch name
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $branch;
|
|
/**
|
|
* VCS credentials
|
|
*
|
|
* @var array|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $credentials;
|
|
/**
|
|
* Version control system type
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $vcs_type;
|
|
/**
|
|
* Whether VCS is self-hosted
|
|
*
|
|
* @var bool
|
|
* @since 1.0.0
|
|
*/
|
|
protected $self_hosted;
|
|
/**
|
|
* Update checker instance
|
|
*
|
|
* @var object|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $update_checker;
|
|
/**
|
|
* Package type
|
|
*
|
|
* @var string
|
|
* @since 1.0.0
|
|
*/
|
|
protected $type;
|
|
/**
|
|
* Content of the packages file for filtering
|
|
*
|
|
* @var string|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $filter_packages_file_content;
|
|
/**
|
|
* License key
|
|
*
|
|
* @var string|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $license_key;
|
|
/**
|
|
* License signature
|
|
*
|
|
* @var string|null
|
|
* @since 1.0.0
|
|
*/
|
|
protected $license_signature;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $server_url The server URL
|
|
* @param string $server_dir The server directory
|
|
* @param string|false $vcs_url The VCS URL
|
|
* @param string $branch The branch name
|
|
* @param array|null $credentials The VCS credentials
|
|
* @param string $vcs_type The VCS type
|
|
* @param bool $self_hosted Whether VCS is self-hosted
|
|
* @since 1.0.0
|
|
*/
|
|
public function __construct( $server_url, $server_dir, $vcs_url, $branch, $credentials, $vcs_type, $self_hosted ) {
|
|
$this->server_dir = $this->normalize_file_path( untrailingslashit( $server_dir ) );
|
|
$this->server_url = $server_url;
|
|
$this->package_dir = $server_dir . 'packages';
|
|
$this->log_dir = $server_dir . 'logs';
|
|
$this->cache = new Cache( $server_dir . 'cache' );
|
|
$this->timezone = new DateTimeZone( wp_timezone_string() );
|
|
$this->server_dir = $server_dir;
|
|
$this->vcs_type = $vcs_type;
|
|
$this->self_hosted = $self_hosted;
|
|
$this->vcs_url = $vcs_url ? trailingslashit( $vcs_url ) : false;
|
|
$this->branch = $branch;
|
|
$this->credentials = $credentials;
|
|
}
|
|
|
|
/*******************************************************************
|
|
* Public methods
|
|
*******************************************************************/
|
|
|
|
/**
|
|
* Process an update API request
|
|
*
|
|
* Handles incoming update requests by initializing, validating, and dispatching them.
|
|
*
|
|
* @param array $query Query parameters
|
|
* @since 1.0.0
|
|
*/
|
|
public function handle_request( $query ) {
|
|
$request = $this->init_request( $query );
|
|
|
|
$this->log_request( $request );
|
|
$this->load_package_for( $request );
|
|
$this->validate_request( $request );
|
|
$this->check_authorization( $request );
|
|
$this->dispatch( $request );
|
|
|
|
exit;
|
|
}
|
|
|
|
// Misc. -------------------------------------------------------
|
|
|
|
/**
|
|
* Get VCS URL
|
|
*
|
|
* @return string|false The VCS URL
|
|
* @since 1.0.0
|
|
*/
|
|
public function get_vcs_url() {
|
|
return $this->vcs_url;
|
|
}
|
|
|
|
/**
|
|
* Get branch name
|
|
*
|
|
* @return string The branch name
|
|
* @since 1.0.0
|
|
*/
|
|
public function get_branch() {
|
|
return $this->branch;
|
|
}
|
|
|
|
/**
|
|
* Pre-filter package information
|
|
*
|
|
* Filter package information before it's processed by the update system.
|
|
*
|
|
* @param array $info Package information
|
|
* @param object $api API instance
|
|
* @param mixed $ref Reference
|
|
* @return array Filtered package information
|
|
* @since 1.0.0
|
|
*/
|
|
public function pre_filter_package_info( $info, $api, $ref ) {
|
|
$abort = true;
|
|
/**
|
|
* Filters the name of the file used to filter the packages retrieved from the VCS
|
|
*
|
|
* @param string $file_name The name of the file used to filter the packages retrieved from the VCS
|
|
* @return string Modified file name
|
|
*/
|
|
$_file = apply_filters( 'upserv_filter_packages_filename', 'updatepulse.json' );
|
|
$file_content = $api->get_remote_file( $_file, $ref );
|
|
|
|
$this->filter_packages_file_content = $file_content;
|
|
|
|
if ( ! empty( $file_content ) ) {
|
|
/**
|
|
* Filters package information before processing
|
|
*
|
|
* Allows modification of package data before it's processed by the update system.
|
|
*
|
|
* @param array $info The package information array
|
|
* @param string $file_content The content of the updatepulse.json file
|
|
* @return array Modified package information
|
|
*/
|
|
$info = apply_filters( 'upserv_pre_filter_package_info', $info, $file_content );
|
|
$file_contents = json_decode( $file_content, true );
|
|
|
|
if (
|
|
is_array( $info ) &&
|
|
! isset( $info['abort_request'] ) &&
|
|
$file_contents &&
|
|
isset( $file_contents['server'] )
|
|
) {
|
|
$url = filter_var( $file_contents['server'], FILTER_VALIDATE_URL );
|
|
$server_url_parts = explode( '/', untrailingslashit( $this->server_url ) );
|
|
|
|
array_pop( $server_url_parts );
|
|
|
|
if ( $url ) {
|
|
$info['server'] = trailingslashit( $url );
|
|
}
|
|
|
|
$server_url = implode( '/', $server_url_parts );
|
|
$abort = ! ( $url && trailingslashit( $server_url ) === trailingslashit( $url ) );
|
|
}
|
|
}
|
|
|
|
if ( $abort ) {
|
|
|
|
if ( is_array( $info ) ) {
|
|
$info['abort_request'] = true;
|
|
} else {
|
|
$info = array( 'abort_request' => true );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fires after pre-filtering package information
|
|
*
|
|
* Allows developers to perform actions after the package information has been initially filtered.
|
|
*
|
|
* @param array $info The package information array after pre-filtering
|
|
*/
|
|
do_action( 'upserv_pre_filter_package_info', $info );
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Filter package information
|
|
*
|
|
* Apply filters to package information after processing.
|
|
*
|
|
* @param array $info Package information
|
|
* @return array Filtered package information
|
|
* @since 1.0.0
|
|
*/
|
|
public function filter_package_info( $info ) {
|
|
/**
|
|
* Filters package information after processing
|
|
*
|
|
* Allows modification of package data after initial processing.
|
|
*
|
|
* @param array $info The package information array
|
|
* @param string $filter_packages_file_content The content of the packages file
|
|
* @return array Modified package information
|
|
*/
|
|
$info = apply_filters( 'upserv_filter_package_info', $info, $this->filter_packages_file_content );
|
|
|
|
/**
|
|
* Fires after filtering package information
|
|
*
|
|
* Allows developers to perform actions after the package information has been filtered.
|
|
*
|
|
* @param array $info The package information array after filtering
|
|
*/
|
|
do_action( 'upserv_filter_package_info', $info );
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Save remote package to local storage
|
|
*
|
|
* Download and save a package from remote repository to local storage.
|
|
*
|
|
* @param string $safe_slug Sanitized package slug
|
|
* @param bool $force Whether to force update even if locked
|
|
* @return bool|mixed Whether the package was saved successfully
|
|
* @since 1.0.0
|
|
*/
|
|
public function save_remote_package_to_local( $safe_slug, $force = false ) {
|
|
$local_ready = false;
|
|
|
|
if ( $force ) {
|
|
self::unlock_update_from_remote( $safe_slug );
|
|
}
|
|
|
|
if ( ! self::is_update_from_remote_locked( $safe_slug ) ) {
|
|
self::lock_update_from_remote( $safe_slug );
|
|
$this->init_update_checker( $safe_slug );
|
|
|
|
if ( $this->update_checker ) {
|
|
|
|
try {
|
|
$info = $this->update_checker->request_info();
|
|
|
|
/**
|
|
* Filters whether to download a remote package
|
|
*
|
|
* Allows plugins to control whether a package should be downloaded from remote repository.
|
|
*
|
|
* @param bool $download Whether to download the package
|
|
* @param string $safe_slug The sanitized package slug
|
|
* @param string $type The package type
|
|
* @param array $info The package information
|
|
* @return bool Whether to proceed with download
|
|
*/
|
|
if ( ! apply_filters(
|
|
'upserv_download_remote_package',
|
|
! ( is_array( $info ) && isset( $info['abort_request'] ) && $info['abort_request'] ),
|
|
$safe_slug,
|
|
$this->type,
|
|
$info
|
|
) ) {
|
|
$this->remove_package( $safe_slug, true );
|
|
|
|
/**
|
|
* Fires when a remote package download is aborted
|
|
*
|
|
* @param string $safe_slug The sanitized package slug
|
|
* @param string $type The package type
|
|
* @param array $info The package information
|
|
*/
|
|
do_action( 'upserv_download_remote_package_aborted', $safe_slug, $this->type, $info );
|
|
|
|
return $info;
|
|
}
|
|
|
|
if ( $info && ! is_wp_error( $info ) ) {
|
|
$this->remove_package( $safe_slug, true );
|
|
|
|
$package = $this->download_remote_package( $info['download_url'] );
|
|
|
|
/**
|
|
* Fires after a remote package has been downloaded
|
|
*
|
|
* @param string $package Path to the downloaded package file
|
|
* @param string $type The package type
|
|
* @param string $safe_slug The sanitized package slug
|
|
*/
|
|
do_action( 'upserv_downloaded_remote_package', $package, $info['type'], $safe_slug );
|
|
|
|
$package_manager = new Zip_Package_Manager(
|
|
$safe_slug,
|
|
$package,
|
|
Data_Manager::get_data_dir( 'tmp' ),
|
|
Data_Manager::get_data_dir( 'packages' )
|
|
);
|
|
$local_ready = $package_manager->clean_package();
|
|
|
|
/**
|
|
* Fires after a remote package has been saved to local storage
|
|
*
|
|
* @param bool $local_ready Whether the package was successfully saved locally
|
|
* @param string $type The package type
|
|
* @param string $safe_slug The sanitized package slug
|
|
*/
|
|
do_action(
|
|
'upserv_saved_remote_package_to_local',
|
|
$local_ready,
|
|
$info['type'],
|
|
$safe_slug
|
|
);
|
|
}
|
|
} catch ( Exception $e ) {
|
|
self::unlock_update_from_remote( $safe_slug );
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
self::unlock_update_from_remote( $safe_slug );
|
|
}
|
|
|
|
return $local_ready;
|
|
}
|
|
|
|
/**
|
|
* Set package type
|
|
*
|
|
* @param string $type Package type
|
|
* @since 1.0.0
|
|
*/
|
|
public function set_type( $type ) {
|
|
$type = is_string( $type ) ? ucfirst( strtolower( $type ) ) : false;
|
|
|
|
if ( 'Plugin' === $type || 'Theme' === $type || 'Generic' === $type ) {
|
|
$this->type = $type;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if remote package needs update
|
|
*
|
|
* Compare local and remote package versions to determine if update is needed.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @return bool|null Whether the package needs update
|
|
* @since 1.0.0
|
|
*/
|
|
public function check_remote_package_update( $slug ) {
|
|
/**
|
|
* Fires before checking if a remote package needs to be updated
|
|
*
|
|
* @param string $slug The package slug
|
|
*/
|
|
do_action( 'upserv_check_remote_update', $slug );
|
|
|
|
$needs_update = true;
|
|
$local_package = $this->find_package( $slug );
|
|
|
|
if ( $local_package instanceof Package ) {
|
|
$package_path = $local_package->get_filename();
|
|
/**
|
|
* Filters the package information gathered from the file system before checking for updates in the VCS
|
|
*
|
|
* @param array $package_info The package information
|
|
* @param Package $package The package object retrieved from the file system
|
|
* @param string $package_slug The slug of the package
|
|
* @return array Modified package information
|
|
*/
|
|
$meta = apply_filters(
|
|
'upserv_check_remote_package_update_local_meta',
|
|
Package_Parser::parse_package( $package_path, true ),
|
|
$local_package,
|
|
$slug
|
|
);
|
|
|
|
if ( ! $meta ) {
|
|
/**
|
|
* Filters whether the package needs to be updated when no metadata is found
|
|
*
|
|
* @param bool $needs_update Whether the package needs to be updated
|
|
* @param Package $package The package object retrieved from the file system
|
|
* @param string $package_slug The slug of the package
|
|
* @return bool Whether the package needs to be updated
|
|
*/
|
|
$needs_update = apply_filters(
|
|
'upserv_check_remote_package_update_no_local_meta_needs_update',
|
|
$needs_update,
|
|
$local_package,
|
|
$slug
|
|
);
|
|
|
|
return $needs_update;
|
|
}
|
|
|
|
$this->set_type( $meta['type'] );
|
|
|
|
if ( 'Plugin' === $this->type || 'Theme' === $this->type || 'Generic' === $this->type ) {
|
|
$this->init_update_checker( $slug );
|
|
|
|
$remote_info = $this->update_checker->request_info();
|
|
|
|
if ( ! is_wp_error( $remote_info ) && isset( $remote_info['version'] ) ) {
|
|
$needs_update = version_compare( $remote_info['version'], $meta['header']['Version'], '>' );
|
|
} else {
|
|
Utils::php_log(
|
|
$remote_info,
|
|
'Invalid value $remote_info for package of type '
|
|
. $this->type . ' and slug ' . $slug
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
$needs_update = null;
|
|
}
|
|
|
|
/**
|
|
* Fires after checking if a remote package needs to be updated
|
|
*
|
|
* @param bool|null $needs_update Whether the package needs to be updated
|
|
* @param string $type The package type
|
|
* @param string $slug The package slug
|
|
*/
|
|
do_action( 'upserv_checked_remote_package_update', $needs_update, $this->type, $slug );
|
|
|
|
return $needs_update;
|
|
}
|
|
|
|
/**
|
|
* Remove a package
|
|
*
|
|
* Delete a package from the filesystem and clear cache.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @param bool $force Whether to force removal even if locked
|
|
* @return bool Whether the package was removed successfully
|
|
* @since 1.0.0
|
|
*/
|
|
public function remove_package( $slug, $force = false ) {
|
|
WP_Filesystem();
|
|
|
|
global $wp_filesystem;
|
|
|
|
if ( $force ) {
|
|
self::unlock_update_from_remote( $slug );
|
|
}
|
|
|
|
if ( self::is_update_from_remote_locked( $slug ) ) {
|
|
return false;
|
|
}
|
|
|
|
self::lock_update_from_remote( $slug );
|
|
|
|
$package_path = trailingslashit( $this->package_dir ) . $slug . '.zip';
|
|
$result = false;
|
|
$type = false;
|
|
$cache_key = false;
|
|
|
|
if ( is_file( $package_path ) ) {
|
|
$cache_key = Zip_Metadata_Parser::build_cache_key( $slug, $package_path );
|
|
$parsed_info = Package_Parser::parse_package( $package_path, true );
|
|
$type = ucfirst( $parsed_info['type'] );
|
|
$result = $wp_filesystem->delete( $package_path );
|
|
}
|
|
|
|
/**
|
|
* Filters whether the package was removed from the file system
|
|
*
|
|
* @param bool $removed Whether the package was removed from the file system
|
|
* @param string $type The type of the package
|
|
* @param string $package_slug The slug of the package
|
|
* @return bool Whether the package was removed from the file system
|
|
*/
|
|
$result = apply_filters( 'upserv_remove_package_result', $result, $type, $slug );
|
|
|
|
if ( $result && $cache_key ) {
|
|
|
|
if ( ! $this->cache ) {
|
|
$this->cache = new Cache( Data_Manager::get_data_dir( 'cache' ) );
|
|
}
|
|
|
|
$this->cache->clear( $cache_key );
|
|
}
|
|
|
|
/**
|
|
* Fires after a package has been removed
|
|
*
|
|
* @param bool $result Whether the package was successfully removed
|
|
* @param string $type The package type
|
|
* @param string $slug The package slug
|
|
*/
|
|
do_action( 'upserv_removed_package', $result, $type, $slug );
|
|
self::unlock_update_from_remote( $slug );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/*******************************************************************
|
|
* Protected methods
|
|
*******************************************************************/
|
|
|
|
/**
|
|
* Add query arguments to a URL
|
|
*
|
|
* Adds or removes query parameters from a URL.
|
|
*
|
|
* @param array $args An associative array of query arguments
|
|
* @param string $url The old URL
|
|
* @return string New URL
|
|
* @since 1.0.0
|
|
*/
|
|
protected static function add_query_arg( $args, $url ) {
|
|
|
|
if ( strpos( $url, '?' ) !== false ) {
|
|
$parts = explode( '?', $url, 2 );
|
|
$base = $parts[0] . '?';
|
|
|
|
parse_str( $parts[1], $query );
|
|
} else {
|
|
$base = $url . '?';
|
|
$query = array();
|
|
}
|
|
|
|
$query = array_merge( $query, $args );
|
|
|
|
//Remove null/false arguments.
|
|
$query = array_filter(
|
|
$query,
|
|
function ( $value ) {
|
|
return ( null !== $value ) && ( false !== $value );
|
|
}
|
|
);
|
|
|
|
return $base . http_build_query( $query, '', '&' );
|
|
}
|
|
|
|
/**
|
|
* Dispatch request to appropriate handler
|
|
*
|
|
* Routes the request to the proper action handler.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function dispatch( $request ) {
|
|
|
|
if ( 'get_metadata' === $request->action ) {
|
|
$this->action_get_metadata( $request );
|
|
} elseif ( 'download' === $request->action ) {
|
|
$this->action_download( $request );
|
|
} else {
|
|
$this->exit_with_error( sprintf( 'Invalid action "%s".', htmlentities( $request->action ) ), 400 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize request
|
|
*
|
|
* Parse and prepare the request parameters.
|
|
*
|
|
* @param array $query Query parameters
|
|
* @return Request Initialized request object
|
|
* @since 1.0.0
|
|
*/
|
|
protected function init_request( $query ) {
|
|
$headers = Headers::parse_current();
|
|
$client_ip = Utils::get_remote_ip();
|
|
$http_method = ! empty( $_SERVER['REQUEST_METHOD'] ) ?
|
|
sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) :
|
|
'GET';
|
|
|
|
if ( ! in_array( $http_method, array( 'GET', 'POST' ), true ) ) {
|
|
$this->exit_with_error( 'Invalid request method.', 405 );
|
|
}
|
|
|
|
$request = new Request( $query, $headers, $client_ip, $http_method );
|
|
|
|
if ( ! upserv_is_package_whitelisted( $request->slug ) && upserv_get_option( 'use_vcs' ) ) {
|
|
$this->exit_with_error( 'Invalid package.', 404 );
|
|
}
|
|
|
|
if ( $request->param( 'type' ) ) {
|
|
$request->type = $request->param( 'type' );
|
|
$this->type = ucfirst( $request->type );
|
|
}
|
|
|
|
$request->token = $request->param( 'token' );
|
|
|
|
return $this->init_license_request( $request );
|
|
}
|
|
|
|
/**
|
|
* Check authorization
|
|
*
|
|
* Verify if the request has proper authorization.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function check_authorization( $request ) {
|
|
|
|
if (
|
|
'download' === $request->action &&
|
|
! upserv_validate_nonce( $request->token )
|
|
) {
|
|
$message = __( 'The download URL token has expired.', 'updatepulse-server' );
|
|
|
|
$this->exit_with_error( $message, 403 );
|
|
}
|
|
|
|
$this->check_license_authorization( $request );
|
|
}
|
|
|
|
/**
|
|
* Generate download URL
|
|
*
|
|
* Create a URL for package download with appropriate parameters.
|
|
*
|
|
* @param Package $package Package instance
|
|
* @return string Download URL
|
|
* @since 1.0.0
|
|
*/
|
|
protected function generate_download_url( Package $package ) {
|
|
$metadata = $package->get_metadata();
|
|
|
|
$this->set_type( $metadata['type'] );
|
|
|
|
$query = $this->filter_license_download_query(
|
|
array(
|
|
'action' => 'download',
|
|
'package_id' => $package->slug,
|
|
'update_type' => $this->type,
|
|
)
|
|
);
|
|
$query['token'] = isset( $query['token'] ) ? $query['token'] : upserv_create_nonce();
|
|
|
|
return self::add_query_arg( $query, $this->server_url );
|
|
}
|
|
|
|
/**
|
|
* Handle download action
|
|
*
|
|
* Process a download request for a package.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function action_download( Request $request ) {
|
|
/**
|
|
* Fires when processing a download action
|
|
*
|
|
* @param Request $request The current request object
|
|
*/
|
|
do_action( 'upserv_update_server_action_download', $request );
|
|
|
|
/**
|
|
* Filters whether the download action has been handled
|
|
*
|
|
* Allows plugins to take over the download action processing.
|
|
*
|
|
* @param bool $handled Whether the download has been handled
|
|
* @param Request $request The current request object
|
|
* @return bool Whether the download has been handled
|
|
*/
|
|
if ( apply_filters( 'upserv_update_server_action_download_handled', false, $request ) ) {
|
|
return;
|
|
}
|
|
|
|
//Required for IE, otherwise Content-Disposition may be ignored.
|
|
if ( ini_get( 'zlib.output_compression' ) ) {
|
|
@ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky, WordPress.PHP.NoSilencedErrors.Discouraged
|
|
}
|
|
|
|
$package = $request->package;
|
|
|
|
header( 'Content-Type: application/zip' );
|
|
header( 'Content-Disposition: attachment; filename=' . $package->slug . '.zip' );
|
|
header( 'Content-Transfer-Encoding: binary' );
|
|
header( 'Content-Length: ' . $package->get_file_size() );
|
|
|
|
readfile( $package->get_filename() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
|
|
}
|
|
|
|
/**
|
|
* Validate request parameters
|
|
*
|
|
* Check if the request contains required parameters.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function validate_request( $request ) {
|
|
|
|
if ( ! $request->action ) {
|
|
$this->exit_with_error( 'You must specify an action.', 400 );
|
|
}
|
|
|
|
if ( ! $request->slug ) {
|
|
$this->exit_with_error( 'You must specify a package slug.', 400 );
|
|
}
|
|
|
|
if ( ! $request->package ) {
|
|
$this->exit_with_error( 'Package not found', 404 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load package for request
|
|
*
|
|
* Find and load the requested package.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function load_package_for( $request ) {
|
|
|
|
if ( empty( $request->slug ) ) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$request->package = $this->find_package( $request->slug );
|
|
} catch ( Invalid_Package_Exception $e ) {
|
|
$this->exit_with_error(
|
|
sprintf(
|
|
'Package "%s" exists, but it is not a valid plugin or theme. ' .
|
|
'Make sure it has the right format ( Zip ) and directory structure.',
|
|
htmlentities( $request->slug )
|
|
)
|
|
);
|
|
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find package by slug
|
|
*
|
|
* Locate a package in local storage or download from remote if needed.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @param bool $check_remote Whether to check remote repositories
|
|
* @return Package|false Package instance or false if not found
|
|
* @since 1.0.0
|
|
*/
|
|
protected function find_package( $slug, $check_remote = true ) {
|
|
|
|
if ( ! $this->cache ) {
|
|
$this->cache = new Cache( Data_Manager::get_data_dir( 'cache' ) );
|
|
}
|
|
|
|
$safe_slug = preg_replace( '@[^a-z0-9\-_\.,+!]@i', '', $slug );
|
|
$package = false;
|
|
$filename = trailingslashit( $this->package_dir ) . $safe_slug . '.zip';
|
|
/**
|
|
* Filters whether to save remote package to local storage
|
|
*
|
|
* Determines if the package should be fetched from remote repository and saved locally.
|
|
*
|
|
* @param bool $save_to_local Whether to save the package locally
|
|
* @param string $safe_slug The sanitized package slug
|
|
* @param string $filename The local filename path
|
|
* @param bool $check_remote Whether to check remote repositories
|
|
* @return bool Whether to save the package locally
|
|
*/
|
|
$save_to_local = apply_filters(
|
|
'upserv_save_remote_to_local',
|
|
! is_file( $filename ) || ! is_readable( $filename ),
|
|
$safe_slug,
|
|
$filename,
|
|
$check_remote
|
|
);
|
|
|
|
if ( upserv_get_option( 'use_vcs' ) && $save_to_local && $check_remote ) {
|
|
$is_package_ready = $this->save_remote_package_to_local( $safe_slug );
|
|
|
|
if ( true === $is_package_ready ) {
|
|
return $this->find_package( $slug, false );
|
|
}
|
|
}
|
|
|
|
try {
|
|
$cached_value = null;
|
|
|
|
if ( is_file( $filename ) && is_readable( $filename ) ) {
|
|
$cache_key = Zip_Metadata_Parser::build_cache_key( $safe_slug, $filename );
|
|
$cached_value = $this->cache->get( $cache_key );
|
|
}
|
|
|
|
if ( null === $cached_value ) {
|
|
/**
|
|
* Fires when no cached package metadata is available
|
|
*
|
|
* @param string $safe_slug The sanitized package slug
|
|
* @param string $filename The local filename path
|
|
* @param Cache $cache The cache instance
|
|
*/
|
|
do_action( 'upserv_find_package_no_cache', $safe_slug, $filename, $this->cache );
|
|
}
|
|
|
|
$package = Package::from_archive( $filename, $safe_slug, $this->cache );
|
|
} catch ( Exception $e ) {
|
|
Utils::php_log( 'Corrupt archive ' . $filename . '; package will not be displayed or delivered' );
|
|
|
|
$log = 'Exception caught: ' . $e->getMessage() . "\n";
|
|
$log .= 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n";
|
|
|
|
Utils::php_log( $log );
|
|
}
|
|
|
|
return $package;
|
|
}
|
|
|
|
/**
|
|
* Handle metadata action
|
|
*
|
|
* Process a request for package metadata.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function action_get_metadata( Request $request ) {
|
|
$meta = array();
|
|
|
|
if ( $request->package ) {
|
|
$meta = $request->package->get_metadata();
|
|
$meta['download_url'] = $this->generate_download_url( $request->package );
|
|
} else {
|
|
$meta['error'] = 'invalid_package';
|
|
$meta['message'] = __( 'Invalid package.', 'updatepulse-server' );
|
|
}
|
|
|
|
$meta = $this->filter_metadata( $meta, $request );
|
|
$meta['time_elapsed'] = Utils::get_time_elapsed();
|
|
|
|
$this->output_as_json( $meta );
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Filter metadata
|
|
*
|
|
* Apply filters to package metadata.
|
|
*
|
|
* @param array $meta Package metadata
|
|
* @param Request $request Request instance
|
|
* @return array Filtered metadata
|
|
* @since 1.0.0
|
|
*/
|
|
protected function filter_metadata( $meta, $request ) {
|
|
$meta = array_filter(
|
|
$meta,
|
|
function ( $value ) {
|
|
return null !== $value;
|
|
}
|
|
);
|
|
|
|
if ( ! isset( $meta['slug'] ) ) {
|
|
return $meta;
|
|
}
|
|
|
|
return $this->filter_license_metadata( $meta, $request );
|
|
}
|
|
|
|
/**
|
|
* Normalize file path
|
|
*
|
|
* Convert all directory separators to forward slashes.
|
|
*
|
|
* @param string $path File path
|
|
* @return string Normalized path
|
|
* @since 1.0.0
|
|
*/
|
|
protected function normalize_file_path( $path ) {
|
|
|
|
if ( ! is_string( $path ) ) {
|
|
return $path;
|
|
}
|
|
|
|
return str_replace( array( DIRECTORY_SEPARATOR, '\\' ), '/', $path );
|
|
}
|
|
|
|
/**
|
|
* Log a request
|
|
*
|
|
* Record details of an API request to log file.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function log_request( $request ) {
|
|
$log_file = $this->get_log_file_name();
|
|
$handle = fopen( $log_file, 'a' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
|
|
|
if ( $handle && flock( $handle, LOCK_EX ) ) {
|
|
$logged_ip = $request->client_ip;
|
|
$columns = array(
|
|
'ip' => $logged_ip,
|
|
'http_method' => $request->http_method,
|
|
'action' => $request->param( 'action', '-' ),
|
|
'slug' => $request->param( 'slug', '-' ),
|
|
'installed_version' => $request->param( 'installed_version', '-' ),
|
|
'wp_version' => isset( $request->wp_version ) ? $request->wp_version : '-',
|
|
'site_url' => isset( $request->wp_site_url ) ? $request->wp_site_url : '-',
|
|
'query' => http_build_query( $request->query, '', '&' ),
|
|
);
|
|
$columns = $this->escape_log_info( $columns );
|
|
|
|
if ( isset( $columns['ip'] ) ) {
|
|
$columns['ip'] = str_pad( $columns['ip'], 15, ' ' );
|
|
}
|
|
|
|
if ( isset( $columns['http_method'] ) ) {
|
|
$columns['http_method'] = str_pad( $columns['http_method'], 4, ' ' );
|
|
}
|
|
|
|
$date = new DateTime( 'now', $this->timezone );
|
|
$line = $date->format( '[Y-m-d H:i:s O]' ) . ' ' . implode( "\t", $columns ) . "\n";
|
|
|
|
fwrite( $handle, $line ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
|
|
flock( $handle, LOCK_UN );
|
|
}
|
|
|
|
if ( $handle ) {
|
|
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get log file name
|
|
*
|
|
* Generate the name of the log file for the current date.
|
|
*
|
|
* @return string Log file path
|
|
* @since 1.0.0
|
|
*/
|
|
protected function get_log_file_name() {
|
|
$path = $this->log_dir . '/request';
|
|
$date = new DateTime( 'now', $this->timezone );
|
|
$path .= '-' . $date->format( 'Y-m-d' );
|
|
|
|
return $path . '.log';
|
|
}
|
|
|
|
/**
|
|
* Escape log information
|
|
*
|
|
* Sanitize data for safe storage in log files.
|
|
*
|
|
* @param string[] $columns List of columns in the log entry
|
|
* @return string[] Escaped columns
|
|
* @since 1.0.0
|
|
*/
|
|
protected function escape_log_info( $columns ) {
|
|
return array_map( array( $this, 'escape_log_value' ), $columns );
|
|
}
|
|
|
|
/**
|
|
* Escape log value
|
|
*
|
|
* Escape a single value for safe storage in log files.
|
|
*
|
|
* @param string|null $value Value to escape
|
|
* @return string|null Escaped value
|
|
* @since 1.0.0
|
|
*/
|
|
protected function escape_log_value( $value ) {
|
|
|
|
if ( ! isset( $value ) ) {
|
|
return null;
|
|
}
|
|
|
|
$value = (string) $value;
|
|
$regex = '/[[:^graph:]]/';
|
|
|
|
//preg_replace_callback will return NULL if the input contains invalid Unicode sequences, so only enable the Unicode flag if the input encoding looks valid.
|
|
if ( function_exists( 'mb_check_encoding' ) && mb_check_encoding( $value, 'UTF-8' ) ) {
|
|
$regex = $regex . 'u';
|
|
}
|
|
|
|
$value = str_replace( '\\', '\\\\', $value );
|
|
$value = preg_replace_callback(
|
|
$regex,
|
|
function ( array $matches ) {
|
|
$length = strlen( $matches[0] );
|
|
$escaped = '';
|
|
|
|
for ( $i = 0; $i < $length; $i++ ) {
|
|
//Convert the character to a hexadecimal escape sequence.
|
|
$hex_code = dechex( ord( $matches[0][ $i ] ) );
|
|
$escaped .= '\x' . strtoupper( str_pad( $hex_code, 2, '0', STR_PAD_LEFT ) );
|
|
}
|
|
|
|
return $escaped;
|
|
},
|
|
$value
|
|
);
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Exit with error
|
|
*
|
|
* Terminate execution and display error message.
|
|
*
|
|
* @param string $message Error message
|
|
* @param int $http_status HTTP status code
|
|
* @since 1.0.0
|
|
*/
|
|
protected function exit_with_error( $message = '', $http_status = 500 ) {
|
|
$status_messages = array(
|
|
// This is not a full list of HTTP status messages. We only need the errors.
|
|
// [Client Error 4xx]
|
|
400 => '400 Bad Request',
|
|
401 => '401 Unauthorized',
|
|
402 => '402 Payment Required',
|
|
403 => '403 Forbidden',
|
|
404 => '404 Not Found',
|
|
405 => '405 Method Not Allowed',
|
|
406 => '406 Not Acceptable',
|
|
407 => '407 Proxy Authentication Required',
|
|
408 => '408 Request Timeout',
|
|
409 => '409 Conflict',
|
|
410 => '410 Gone',
|
|
411 => '411 Length Required',
|
|
412 => '412 Precondition Failed',
|
|
413 => '413 Request Entity Too Large',
|
|
414 => '414 Request-URI Too Long',
|
|
415 => '415 Unsupported Media Type',
|
|
416 => '416 Requested Range Not Satisfiable',
|
|
417 => '417 Expectation Failed',
|
|
// [Server Error 5xx]
|
|
500 => '500 Internal Server Error',
|
|
501 => '501 Not Implemented',
|
|
502 => '502 Bad Gateway',
|
|
503 => '503 Service Unavailable',
|
|
504 => '504 Gateway Timeout',
|
|
505 => '505 HTTP Version Not Supported',
|
|
);
|
|
|
|
$protocol = empty( $_SERVER['SERVER_PROTOCOL'] ) ? 'HTTP/1.1' : sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) );
|
|
|
|
//Output a HTTP status header.
|
|
if ( isset( $status_messages[ $http_status ] ) ) {
|
|
header( $protocol . ' ' . $status_messages[ $http_status ] );
|
|
$title = $status_messages[ $http_status ];
|
|
} else {
|
|
header( 'X-Ws-Update-Server-Error: ' . $http_status, true, $http_status );
|
|
$title = 'HTTP ' . $http_status;
|
|
}
|
|
|
|
if ( '' === $message ) {
|
|
$message = $title;
|
|
}
|
|
|
|
//And a basic HTML error message.
|
|
printf(
|
|
'<html>
|
|
<head> <title>%1$s</title> </head>
|
|
<body> <h1>%1$s</h1> <p>%2$s</p> </body>
|
|
</html>',
|
|
esc_html( $title ),
|
|
esc_html( $message )
|
|
);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Output data as JSON
|
|
*
|
|
* Send data as JSON response with appropriate headers.
|
|
*
|
|
* @param mixed $response Response data
|
|
* @since 1.0.0
|
|
*/
|
|
protected function output_as_json( $response ) {
|
|
header( 'Content-Type: application/json; charset=utf-8' );
|
|
|
|
echo wp_json_encode( $response, Utils::JSON_OPTIONS ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
}
|
|
|
|
// Misc. -------------------------------------------------------
|
|
|
|
/**
|
|
* Unlock update from remote
|
|
*
|
|
* Remove lock for remote update process.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @since 1.0.0
|
|
*/
|
|
protected static function unlock_update_from_remote( $slug ) {
|
|
$locks = get_option( 'upserv_update_from_remote_locks' );
|
|
$locks = is_array( $locks ) ? $locks : array();
|
|
|
|
if ( array_key_exists( $slug, $locks ) ) {
|
|
unset( $locks[ $slug ] );
|
|
}
|
|
|
|
update_option( 'upserv_update_from_remote_locks', $locks );
|
|
}
|
|
|
|
/**
|
|
* Lock update from remote
|
|
*
|
|
* Create lock for remote update process.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @since 1.0.0
|
|
*/
|
|
protected static function lock_update_from_remote( $slug ) {
|
|
$locks = get_option( 'upserv_update_from_remote_locks', array() );
|
|
$locks = is_array( $locks ) ? $locks : array();
|
|
|
|
if ( ! array_key_exists( $slug, $locks ) ) {
|
|
$locks[ $slug ] = time() + self::LOCK_REMOTE_UPDATE_SEC;
|
|
|
|
update_option( 'upserv_update_from_remote_locks', $locks );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if update from remote is locked
|
|
*
|
|
* Determine if there's an active lock for remote update.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @return bool Whether the update is locked
|
|
* @since 1.0.0
|
|
*/
|
|
protected static function is_update_from_remote_locked( $slug ) {
|
|
$locks = get_option( 'upserv_update_from_remote_locks' );
|
|
$is_locked = is_array( $locks ) && array_key_exists( $slug, $locks ) && $locks[ $slug ] >= time();
|
|
|
|
return $is_locked;
|
|
}
|
|
|
|
/**
|
|
* Build update checker
|
|
*
|
|
* Create instance of appropriate update checker for package type.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @param string $package_filename Package filename
|
|
* @return object|false Update checker instance or false if not supported
|
|
* @since 1.0.0
|
|
*/
|
|
protected function build_update_checker( $slug, $package_filename ) {
|
|
$repo_url = trailingslashit( $this->vcs_url ) . $slug;
|
|
$service = upserv_get_vcs_name( $this->vcs_type, 'edit' );
|
|
$api_class = $service ? 'Anyape\PackageUpdateChecker\Vcs\\' . $service . 'Api' : false;
|
|
|
|
if ( ! $api_class ) {
|
|
return false;
|
|
}
|
|
|
|
$checker_class = 'Anyape\PackageUpdateChecker\\' . $this->type . 'UpdateChecker';
|
|
$params = array( new $api_class( $repo_url ), $slug, $this->package_dir );
|
|
|
|
if ( $package_filename ) {
|
|
$params[] = $package_filename;
|
|
}
|
|
|
|
return new $checker_class( ...$params );
|
|
}
|
|
|
|
/**
|
|
* Initialize update checker
|
|
*
|
|
* Set up the update checker with appropriate credentials and settings.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @since 1.0.0
|
|
*/
|
|
protected function init_update_checker( $slug ) {
|
|
/**
|
|
* Filters the checker object used to perform remote checks and downloads
|
|
*
|
|
* @param mixed $update_checker The checker object
|
|
* @param string $package_slug The slug of the package using the checker object
|
|
* @param string $type The type of the package using the checker object
|
|
* @param string $vcs_url URL of the VCS where the remote packages are located
|
|
* @param string $branch The branch of the VCS repository where the packages are located
|
|
* @param mixed $credentials The credentials to access the VCS where the packages are located
|
|
* @param bool $self_hosted Whether the VCS is self-hosted
|
|
* @return mixed Modified update checker object
|
|
*/
|
|
$this->update_checker = apply_filters(
|
|
'upserv_update_checker',
|
|
$this->update_checker,
|
|
$slug,
|
|
$this->type,
|
|
$this->vcs_url,
|
|
$this->branch,
|
|
$this->credentials,
|
|
$this->self_hosted
|
|
);
|
|
|
|
if ( $this->update_checker && $this->update_checker->slug === $slug ) {
|
|
return;
|
|
}
|
|
|
|
require_once UPSERV_PLUGIN_PATH . 'lib/package-update-checker/package-update-checker.php';
|
|
|
|
$package_filename = null;
|
|
|
|
if ( 'Plugin' === $this->type ) {
|
|
$package_filename = $slug;
|
|
} elseif ( 'Generic' === $this->type ) {
|
|
$package_filename = 'updatepulse';
|
|
}
|
|
|
|
$this->update_checker = $this->build_update_checker( $slug, $package_filename );
|
|
|
|
if ( $this->update_checker ) {
|
|
|
|
if ( $this->credentials ) {
|
|
$this->update_checker->set_authentication( $this->credentials );
|
|
}
|
|
|
|
$this->update_checker->set_branch( $this->branch );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download remote package
|
|
*
|
|
* Fetch a package file from a remote URL.
|
|
*
|
|
* @param string $url Remote file URL
|
|
* @param int $timeout Request timeout in seconds
|
|
* @return string|WP_Error Local filename or error
|
|
* @since 1.0.0
|
|
*/
|
|
protected function download_remote_package( $url, $timeout = 300 ) {
|
|
|
|
if ( ! $url ) {
|
|
return new WP_Error( 'http_no_url', __( 'Invalid URL provided.', 'updatepulse-server' ) );
|
|
}
|
|
|
|
$local_filename = wp_tempnam( $url );
|
|
|
|
if ( ! $local_filename ) {
|
|
return new WP_Error( 'http_no_file', __( 'Could not create temporary file.', 'updatepulse-server' ) );
|
|
}
|
|
|
|
$params = array(
|
|
'timeout' => $timeout,
|
|
'stream' => true,
|
|
'filename' => $local_filename,
|
|
);
|
|
|
|
if ( $this->credentials ) {
|
|
$auth_headers = $this->update_checker->get_vcs_api()->get_authorization_headers();
|
|
|
|
if ( $auth_headers ) {
|
|
$params['headers'] = $auth_headers;
|
|
}
|
|
}
|
|
|
|
$response = wp_safe_remote_get( $url, $params );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
wp_delete_file( $local_filename );
|
|
Utils::php_log( $response, 'Invalid value for $response' );
|
|
|
|
return $response;
|
|
}
|
|
|
|
if ( 200 !== abs( intval( wp_remote_retrieve_response_code( $response ) ) ) ) {
|
|
wp_delete_file( $local_filename );
|
|
|
|
return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ) );
|
|
}
|
|
|
|
$content_md5 = wp_remote_retrieve_header( $response, 'content-md5' );
|
|
|
|
if ( $content_md5 ) {
|
|
$md5_check = verify_file_md5( $local_filename, $content_md5 );
|
|
|
|
if ( is_wp_error( $md5_check ) ) {
|
|
wp_delete_file( $local_filename );
|
|
Utils::php_log( $md5_check, 'Invalid value for $md5_check' );
|
|
|
|
return $md5_check;
|
|
}
|
|
}
|
|
|
|
return $local_filename;
|
|
}
|
|
|
|
// Licenses -------------------------------------------------------
|
|
|
|
/**
|
|
* Initialize license request
|
|
*
|
|
* Prepare a request with license information.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @return Request Modified request with license data
|
|
* @since 1.0.0
|
|
*/
|
|
protected function init_license_request( $request ) {
|
|
|
|
if ( ! $request->param( 'license_key' ) ) {
|
|
return $request;
|
|
}
|
|
|
|
$result = false;
|
|
|
|
if ( $request->param( 'licensed_with' ) ) {
|
|
$info = upserv_get_package_info( $request->slug, false );
|
|
|
|
if (
|
|
$info &&
|
|
isset( $info['licensed_with'] ) &&
|
|
$request->param( 'licensed_with' ) === $info['licensed_with']
|
|
) {
|
|
$main_package_info = upserv_get_package_info( $info['licensed_with'], false );
|
|
$result = $this->verify_license_exists(
|
|
$info['licensed_with'],
|
|
$main_package_info['type'],
|
|
$request->param( 'license_key' )
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( ! $result ) {
|
|
$result = $this->verify_license_exists(
|
|
$request->slug,
|
|
$request->type,
|
|
$request->param( 'license_key' )
|
|
);
|
|
}
|
|
|
|
$request->license_key = $request->param( 'license_key' );
|
|
$request->license_signature = $request->param( 'license_signature' );
|
|
$request->license = $result;
|
|
|
|
$this->license_key = $request->license_key;
|
|
$this->license_signature = $request->license_signature;
|
|
|
|
return $request;
|
|
}
|
|
|
|
/**
|
|
* Filter license metadata
|
|
*
|
|
* Modify metadata based on license status.
|
|
*
|
|
* @param array $meta Package metadata
|
|
* @param Request $request Request instance
|
|
* @return array Filtered metadata
|
|
* @since 1.0.0
|
|
*/
|
|
protected function filter_license_metadata( $meta, $request ) {
|
|
|
|
if ( ! upserv_is_package_require_license( $meta['slug'] ) ) {
|
|
return $meta;
|
|
}
|
|
|
|
$license = $request->license;
|
|
$license_signature = $request->license_signature;
|
|
|
|
if ( is_object( $license ) || is_array( $license ) ) {
|
|
$meta['license'] = $this->prepare_license_for_output( $license );
|
|
}
|
|
|
|
if (
|
|
/**
|
|
* Filters whether a license is valid
|
|
*
|
|
* Allows plugins to override license validation logic.
|
|
*
|
|
* @param bool $is_valid Whether the license is valid based on internal validation
|
|
* @param object $license The license object
|
|
* @param string $license_signature The license signature
|
|
* @return bool Whether the license is valid
|
|
*/
|
|
apply_filters(
|
|
'upserv_license_valid',
|
|
$this->is_license_valid( $license, $license_signature ),
|
|
$license,
|
|
$license_signature
|
|
)
|
|
) {
|
|
$args = array(
|
|
'license_key' => $request->license_key,
|
|
'license_signature' => $request->license_signature,
|
|
);
|
|
$meta['download_url'] = self::add_query_arg( $args, $meta['download_url'] );
|
|
} else {
|
|
unset( $meta['download_url'] );
|
|
unset( $meta['license'] );
|
|
|
|
$meta['license_error'] = $this->get_license_error( $license );
|
|
}
|
|
|
|
return $meta;
|
|
}
|
|
|
|
/**
|
|
* Filter license download query
|
|
*
|
|
* Add license parameters to download URL.
|
|
*
|
|
* @param array $query Query parameters
|
|
* @return array Modified query parameters
|
|
* @since 1.0.0
|
|
*/
|
|
protected function filter_license_download_query( $query ) {
|
|
|
|
if ( upserv_is_package_require_license( $query['package_id'] ) ) {
|
|
$query['token'] = upserv_create_nonce( true, DAY_IN_SECONDS / 2 );
|
|
$query['license_key'] = $this->license_key;
|
|
$query['license_signature'] = $this->license_signature;
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Check license authorization
|
|
*
|
|
* Verify if the license is valid for the requested action.
|
|
*
|
|
* @param Request $request Request instance
|
|
* @since 1.0.0
|
|
*/
|
|
protected function check_license_authorization( $request ) {
|
|
|
|
if ( ! upserv_is_package_require_license( $request->slug ) ) {
|
|
return;
|
|
}
|
|
|
|
$license = $request->license;
|
|
$license_signature = $request->license_signature;
|
|
$valid = $this->is_license_valid( $license, $license_signature );
|
|
|
|
if (
|
|
'download' === $request->action &&
|
|
/**
|
|
* Filters whether a license is valid when requesting for an update
|
|
*
|
|
* @param bool $is_valid Whether the license is valid
|
|
* @param mixed $license The license to validate
|
|
* @param string $license_signature The signature of the license
|
|
* @return bool Whether the license is valid
|
|
*/
|
|
! apply_filters( 'upserv_license_valid', $valid, $license, $license_signature )
|
|
) {
|
|
$this->exit_with_error( 'Invalid license key or signature.', 403 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get license error
|
|
*
|
|
* Format error information for invalid license.
|
|
*
|
|
* @param mixed $license License data or error
|
|
* @return object Error information
|
|
* @since 1.0.0
|
|
*/
|
|
protected function get_license_error( $license ) {
|
|
|
|
if ( is_wp_error( $license ) ) {
|
|
$error = (object) array(
|
|
'code' => 'license_error',
|
|
'message' => implode( '<br>', $license->get_error_messages() ),
|
|
'data' => (object) array(
|
|
'license' => $license,
|
|
),
|
|
);
|
|
} elseif ( is_object( $license ) && 'activated' !== $license->status ) {
|
|
$error = (object) array(
|
|
'code' => 'illegal_license_status',
|
|
'message' => 'The license cannot be used for the requested action.',
|
|
'data' => (object) array(
|
|
'license' => $license,
|
|
),
|
|
);
|
|
} else {
|
|
$error = (object) array(
|
|
'code' => 'invalid_license',
|
|
'message' => 'The license key or signature is invalid.',
|
|
'data' => (object) array(
|
|
'license' => $license,
|
|
),
|
|
);
|
|
}
|
|
|
|
return $error;
|
|
}
|
|
|
|
/**
|
|
* Verify license exists
|
|
*
|
|
* Check if a license exists and is valid for the package.
|
|
*
|
|
* @param string $slug Package slug
|
|
* @param string $type Package type
|
|
* @param string $license_key License key
|
|
* @return object|false License data or false if invalid
|
|
* @since 1.0.0
|
|
*/
|
|
protected function verify_license_exists( $slug, $type, $license_key ) {
|
|
$license_server = new License_Server();
|
|
$payload = array( 'license_key' => $license_key );
|
|
$result = $license_server->read_license( $payload );
|
|
|
|
if (
|
|
is_object( $result ) &&
|
|
$slug === $result->package_slug &&
|
|
$type &&
|
|
$result->package_type &&
|
|
strtolower( $type ) === strtolower( $result->package_type )
|
|
) {
|
|
$result->result = 'success';
|
|
$result->message = __( 'License key details retrieved.', 'updatepulse-server' );
|
|
} else {
|
|
$result = false;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Prepare license for output
|
|
*
|
|
* Filter sensitive data from license information.
|
|
*
|
|
* @param object|array $license License data
|
|
* @return array License data safe for output
|
|
* @since 1.0.0
|
|
*/
|
|
protected function prepare_license_for_output( $license ) {
|
|
$output = json_decode( wp_json_encode( $license ), true );
|
|
|
|
unset( $output['id'] );
|
|
unset( $output['hmac_key'] );
|
|
unset( $output['crypto_key'] );
|
|
unset( $output['data'] );
|
|
unset( $output['owner_name'] );
|
|
unset( $output['email'] );
|
|
unset( $output['company_name'] );
|
|
|
|
/**
|
|
* Filters license data prepared for output
|
|
*
|
|
* Allows modification of license information before sending to client.
|
|
*
|
|
* @param array $output The prepared license data with sensitive information removed
|
|
* @param object|array $license The original license data
|
|
* @return array Modified license data for output
|
|
*/
|
|
return apply_filters( 'upserv_license_update_server_prepare_license_for_output', $output, $license );
|
|
}
|
|
|
|
/**
|
|
* Check if license is valid
|
|
*
|
|
* Verify license key and signature.
|
|
*
|
|
* @param object $license License data
|
|
* @param string $license_signature License signature
|
|
* @return bool Whether the license is valid
|
|
* @since 1.0.0
|
|
*/
|
|
protected function is_license_valid( $license, $license_signature ) {
|
|
$valid = false;
|
|
|
|
if ( is_object( $license ) && ! is_wp_error( $license ) && 'activated' === $license->status ) {
|
|
|
|
/**
|
|
* Filters whether to bypass signature validation
|
|
*
|
|
* Allows plugins to skip signature validation for certain licenses.
|
|
*
|
|
* @param bool $bypass Whether to bypass signature validation
|
|
* @param object $license The license object
|
|
* @return bool Whether to bypass signature validation
|
|
*/
|
|
if ( apply_filters( 'upserv_license_bypass_signature', false, $license ) ) {
|
|
$valid = $this->license_key === $license->license_key;
|
|
} else {
|
|
$license_server = new License_Server();
|
|
$valid = $this->license_key === $license->license_key &&
|
|
$license_server->is_signature_valid( $license->license_key, $license_signature );
|
|
}
|
|
}
|
|
|
|
return $valid;
|
|
}
|
|
}
|