From 80097427dcb35872b9745da1c1d80a6478e04228 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:46:50 +0800 Subject: [PATCH 01/49] require_license API standardization --- docs/packages.md | 22 +++++++++---------- .../update/class-zip-metadata-parser.php | 9 ++++---- lib/anyape-package-parser/package-parser.php | 4 +++- readme.txt | 3 +++ updatepulse-server.php | 2 +- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/packages.md b/docs/packages.md index 1aed023..15a7cf0 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -486,7 +486,7 @@ Values format in case of a plugin package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "plugin-slug", "type": "plugin", "file_name": "plugin-slug.zip", @@ -523,7 +523,7 @@ Values format in case of a theme package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "theme-slug", "type": "theme", "file_name": "theme-slug.zip", @@ -560,7 +560,7 @@ Values format in case of a generic package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "generic-slug", "type": "generic", "file_name": "generic-slug.zip", @@ -650,7 +650,7 @@ Values format in case of a plugin package: "low": "https:\/\/domain.tld\/banner-722x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "plugin-slug", "type": "plugin", "file_name": "plugin-slug.zip", @@ -687,7 +687,7 @@ Values format in case of a theme package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "theme-slug", "type": "theme", "file_name": "theme-slug.zip", @@ -724,7 +724,7 @@ Values format in case of a generic package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "generic-slug", "type": "generic", "file_name": "generic-slug.zip", @@ -885,7 +885,7 @@ Values format in case of a theme package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "theme-slug", "type": "theme", "file_name": "theme-slug.zip", @@ -922,7 +922,7 @@ Values format in case of a generic package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "generic-slug", "type": "generic", "file_name": "generic-slug.zip", @@ -1426,7 +1426,7 @@ Values format in case of a plugin package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "plugin-slug", "type": "plugin", "file_name": "plugin-slug.zip", @@ -1454,7 +1454,7 @@ Values format in case of a theme package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "theme-slug", "type": "theme", "file_name": "theme-slug.zip", @@ -1482,7 +1482,7 @@ Values format in case of a generic package: "low": "https:\/\/domain.tld\/banner-772x250.png", "high": "https:\/\/domain.tld\/banner-1544x500.png" }, - "require_license": false, + "require_license": "", "slug": "generic-slug", "type": "generic", "file_name": "generic-slug.zip", diff --git a/inc/server/update/class-zip-metadata-parser.php b/inc/server/update/class-zip-metadata-parser.php index 2dd0d80..3900344 100644 --- a/inc/server/update/class-zip-metadata-parser.php +++ b/inc/server/update/class-zip-metadata-parser.php @@ -324,13 +324,14 @@ class Zip_Metadata_Parser { $this->metadata['banners'] = $extra_meta['banners']; } - if ( ! empty( $extra_meta['require_license'] ) ) { - $this->metadata['require_license'] = ( + $this->metadata['require_license'] = ( + ! empty( $extra_meta['require_license'] ) && + ( 'yes' === $extra_meta['require_license'] || 'true' === $extra_meta['require_license'] || 1 === intval( $extra_meta['require_license'] ) - ); - } + ) + ); if ( ! empty( $extra_meta['licensed_with'] ) ) { $this->metadata['licensed_with'] = $extra_meta['licensed_with']; diff --git a/lib/anyape-package-parser/package-parser.php b/lib/anyape-package-parser/package-parser.php index f03abf4..ce1e97a 100644 --- a/lib/anyape-package-parser/package-parser.php +++ b/lib/anyape-package-parser/package-parser.php @@ -521,7 +521,9 @@ class Parser { $extra_headers = array(); if ( ! empty( $headers['RequireLicense'] ) ) { - $extra_headers['require_license'] = $headers['RequireLicense']; + $extra_headers['require_license'] = (bool) $headers['RequireLicense']; + } else { + $extra_headers['require_license'] = false; } if ( ! empty( $headers['LicensedWith'] ) ) { diff --git a/readme.txt b/readme.txt index 16df68c..d8ad23d 100644 --- a/readme.txt +++ b/readme.txt @@ -65,6 +65,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.3 = +* Minor Package API fix + = 1.0.2 = * Minor Package API fix * Minor License API fix diff --git a/updatepulse-server.php b/updatepulse-server.php index a1b30b0..a0dd77f 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.2 + * Version: 1.0.3 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From b3dd2c6f536a462e6deab25bcc76a535c8554b3f Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:36:15 +0800 Subject: [PATCH 02/49] remove JSON_NUMERIC_CHECK for API output --- inc/class-utils.php | 2 +- inc/server/update/class-update-server.php | 2 +- readme.txt | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/inc/class-utils.php b/inc/class-utils.php index 0dfc6da..67f3b73 100644 --- a/inc/class-utils.php +++ b/inc/class-utils.php @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { class Utils { // JSON options - const JSON_OPTIONS = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK; + const JSON_OPTIONS = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE; /** * @param string $message diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php index c13249f..fdaaa69 100644 --- a/inc/server/update/class-update-server.php +++ b/inc/server/update/class-update-server.php @@ -756,7 +756,7 @@ class Update_Server { protected function output_as_json( $response ) { header( 'Content-Type: application/json; charset=utf-8' ); - echo wp_json_encode( $response, JSON_PRETTY_PRINT ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo wp_json_encode( $response, Utils::JSON_OPTIONS ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } // Misc. ------------------------------------------------------- diff --git a/readme.txt b/readme.txt index d8ad23d..269fdae 100644 --- a/readme.txt +++ b/readme.txt @@ -67,6 +67,7 @@ This section describes how to install the plugin and get it working. = 1.0.3 = * Minor Package API fix +* All API: remove `JSON_NUMERIC_CHECK` when encoding output as it creates issues with values like version numbers. = 1.0.2 = * Minor Package API fix From 1e3add450e5f43b430433836e58b8b843a78101f Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sun, 2 Mar 2025 09:23:34 +0800 Subject: [PATCH 03/49] Fix deprecated PHP 8.3 calls to `get_class()` + Add a URL to test the Update API endpoint in Packages JSON details + Minor code cleanup --- inc/nonce/class-nonce.php | 12 ++++++------ inc/table/class-packages-table.php | 16 ++++++++++++++-- readme.txt | 3 +++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/inc/nonce/class-nonce.php b/inc/nonce/class-nonce.php index 1db63e7..f639593 100644 --- a/inc/nonce/class-nonce.php +++ b/inc/nonce/class-nonce.php @@ -106,7 +106,7 @@ class Nonce { if ( is_string( $wp->query_vars['action'] ) && method_exists( - get_class(), + __CLASS__, 'generate_' . $wp->query_vars['action'] . '_api_response' ) ) { @@ -194,14 +194,14 @@ class Nonce { public static function register() { if ( ! self::is_doing_api_request() ) { - add_action( 'upserv_scheduler_init', array( get_class(), 'upserv_scheduler_init' ) ); - add_action( 'upserv_nonce_cleanup', array( get_class(), 'upserv_nonce_cleanup' ) ); + add_action( 'upserv_scheduler_init', array( __CLASS__, 'upserv_scheduler_init' ) ); + add_action( 'upserv_nonce_cleanup', array( __CLASS__, 'upserv_nonce_cleanup' ) ); } - add_action( 'init', array( get_class(), 'add_endpoints' ) ); - add_action( 'parse_request', array( get_class(), 'parse_request' ), -99, 0 ); + add_action( 'init', array( __CLASS__, 'add_endpoints' ) ); + add_action( 'parse_request', array( __CLASS__, 'parse_request' ), -99, 0 ); - add_filter( 'query_vars', array( get_class(), 'query_vars' ), -99, 1 ); + add_filter( 'query_vars', array( __CLASS__, 'query_vars' ), -99, 1 ); } public static function init_auth( $private_keys ) { diff --git a/inc/table/class-packages-table.php b/inc/table/class-packages-table.php index c253bff..4f49c50 100644 --- a/inc/table/class-packages-table.php +++ b/inc/table/class-packages-table.php @@ -148,6 +148,19 @@ class Packages_Table extends WP_List_Table { $record['metadata']['origin'] = 'unknown'; } + $record['update_url'] = add_query_arg( + array( + 'action' => 'get_metadata', + 'package_id' => $record['slug'], + 'installed_version' => $record['version'], + 'php' => PHP_VERSION, + 'locale' => get_locale(), + 'checking_for_updates' => 1, + 'update_type' => ucfirst( $record['type'] ), + ), + home_url( '/updatepulse-server-update-api/' ) + ); + $info = $record; $unset_metadata = array( 'previous', 'branch', 'vcs_key', 'vcs', 'whitelist' ); @@ -245,8 +258,7 @@ class Packages_Table extends WP_List_Table { } protected function get_table_classes() { - $mode = get_user_setting( 'posts_list_mode', 'list' ); - + $mode = get_user_setting( 'posts_list_mode', 'list' ); $mode_class = esc_attr( 'table-view-' . $mode ); return array( 'widefat', 'striped', $mode_class, $this->_args['plural'] ); diff --git a/readme.txt b/readme.txt index 269fdae..3b3f8be 100644 --- a/readme.txt +++ b/readme.txt @@ -68,6 +68,9 @@ This section describes how to install the plugin and get it working. = 1.0.3 = * Minor Package API fix * All API: remove `JSON_NUMERIC_CHECK` when encoding output as it creates issues with values like version numbers. +* Fix deprecated PHP 8.3 calls to `get_class()` +* Add a URL to test the Update API endpoint in Packages JSON details +* Minor code cleanup = 1.0.2 = * Minor Package API fix From 3c2968a45388ac2f48ad68ee05195f07a898b818 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sun, 2 Mar 2025 09:42:03 +0800 Subject: [PATCH 04/49] version bump --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 3b3f8be..e0cc03b 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.2 +Stable tag: 1.0.3 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html From 8f9523428582e81a4b6df2ed740860885c192e96 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 3 Mar 2025 01:07:33 +0800 Subject: [PATCH 05/49] Full documentation WIP --- autoload.php | 21 + docs/misc.md | 2 +- functions.php | 545 +++++++++++++++++- lib/anyape-crypto/crypto.php | 59 +- lib/anyape-package-parser/package-parser.php | 301 +++++----- lib/package-update-checker/Autoloader.php | 22 + .../GenericUpdateChecker.php | 31 + .../PluginUpdateChecker.php | 34 ++ .../ThemeUpdateChecker.php | 30 + lib/package-update-checker/UpdateChecker.php | 80 ++- lib/package-update-checker/Vcs/Api.php | 25 +- .../Vcs/BitbucketApi.php | 76 ++- lib/package-update-checker/Vcs/GitHubApi.php | 155 +++-- lib/package-update-checker/Vcs/GitLabApi.php | 139 +++-- lib/package-update-checker/Vcs/Reference.php | 32 +- .../Vcs/ReleaseAssetSupport.php | 40 +- .../Vcs/ReleaseFilteringFeature.php | 48 +- .../Vcs/VcsCheckerMethods.php | 26 +- .../class-wp-update-migrate.php | 153 ++++- optimisation/upserv-default-optimizer.php | 27 + tests.php | 81 ++- uninstall.php | 33 ++ updatepulse-server.php | 46 ++ 23 files changed, 1648 insertions(+), 358 deletions(-) diff --git a/autoload.php b/autoload.php index b2684d1..9fc8ec2 100644 --- a/autoload.php +++ b/autoload.php @@ -1,7 +1,27 @@ get_options(); } } if ( ! function_exists( 'upserv_update_options' ) ) { + /** + * Updates all plugin options + * + * Replaces the entire options array with the provided options. + * + * @since 1.0 + * + * @param array $options The new options to save + * @return bool True on success, false on failure + */ function upserv_update_options( $options ) { return UPServ::get_instance()->update_options( $options ); } } if ( ! function_exists( 'upserv_get_option' ) ) { + /** + * Gets a specific option by path + * + * Retrieves an option value using slash notation path. + * + * @since 1.0 + * + * @param string $path The path to the option using slash notation + * @param mixed $_default Default value if option doesn't exist + * @return mixed The option value or default if not found + */ function upserv_get_option( $path, $_default = null ) { return UPServ::get_instance()->get_option( $path, $_default ); } } if ( ! function_exists( 'upserv_set_option' ) ) { + /** + * Sets a specific option by path + * + * Set an option value within the current request using slash notation path. + * Does NOT commit the changes to persistence. + * To persist the data, call @see upserv_update_options() + * with the return value of this function. + * + * @since 1.0 + * + * @param string $path The path to the option using slash notation + * @param mixed $value The value to set + * @return array The updated options array + */ function upserv_set_option( $path, $value ) { return UPServ::get_instance()->set_option( $path, $value ); } } if ( ! function_exists( 'upserv_update_option' ) ) { + /** + * Updates a specific option by path + * + * Updates an existing option value using slash notation path. + * Commits the changes to persistence. + * + * @since 1.0 + * + * @param string $path The path to the option using slash notation + * @param mixed $value The value to set + * @return bool True on success, false on failure + */ function upserv_update_option( $path, $value ) { return UPServ::get_instance()->update_option( $path, $value ); } } if ( ! function_exists( 'upserv_assets_suffix' ) ) { + /** + * Gets the appropriate asset file suffix based on debug mode + * + * Returns an empty string in debug mode, or '.min' in production, + * to be used for loading appropriate CSS/JS file versions. + * + * @since 1.0 + * + * @return string '.min' if WP_DEBUG is false, empty string otherwise + */ function upserv_assets_suffix() { return (bool) ( constant( 'WP_DEBUG' ) ) ? '' : '.min'; } @@ -78,30 +165,68 @@ if ( ! function_exists( 'upserv_assets_suffix' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_is_doing_license_api_request' ) ) { + /** + * Determines if the current request is a License API request + * + * Checks whether the current request is made by a client plugin or theme + * interacting with the plugin's license API. + * + * @since 1.0 + * + * @return bool True if the current request is a License API request, false otherwise + */ function upserv_is_doing_license_api_request() { return License_API::is_doing_api_request(); } } if ( ! function_exists( 'upserv_is_doing_update_api_request' ) ) { + /** + * Determine whether the current request is made by a client plugin, theme, or generic package interacting with the plugin's API. + * + * @since 1.0 + * + * @return bool `true` if the current request is a client plugin, theme, or generic package interacting with the plugin's API, `false` otherwise + */ function upserv_is_doing_update_api_request() { return Update_API::is_doing_api_request(); } } if ( ! function_exists( 'upserv_is_doing_webhook_api_request' ) ) { + /** + * Determine whether the current request is made by a Webhook. + * + * @since 1.0 + * + * @return bool `true` if the current request is made by a Webhook, `false` otherwise + */ function upserv_is_doing_webhook_api_request() { return Webhook_API::is_doing_api_request(); } } if ( ! function_exists( 'upserv_is_doing_package_api_request' ) ) { + /** + * Determine whether the current request is made by a remote client interacting with the plugin's package API. + * + * @since 1.0 + * + * @return bool `true` if the current request is made by a remote client interacting with the plugin's package API, `false` otherwise + */ function upserv_is_doing_package_api_request() { return Package_API::is_doing_api_request(); } } if ( ! function_exists( 'upserv_is_doing_api_request' ) ) { + /** + * Determine whether the current request is made by a remote client interacting with any of the APIs. + * + * @since 1.0 + * + * @return bool `true` if the current request is made by a remote client interacting with any of the APIs, `false` otherwise + */ function upserv_is_doing_api_request() { $mu_doing_api = wp_cache_get( 'upserv_mu_doing_api', 'updatepulse-server' ); $is_api_request = $mu_doing_api ? @@ -118,10 +243,18 @@ if ( ! function_exists( 'upserv_is_doing_api_request' ) ) { } /******************************************************************* - * Data ditectories functions + * Data directories functions *******************************************************************/ if ( ! function_exists( 'upserv_get_data_dir' ) ) { + /** + * Get the path to a specific directory within the plugin's content directory. + * + * @since 1.0 + * + * @param string $dir The directory to get the path for + * @return string The path to the specified directory within the plugin's content directory + */ function upserv_get_data_dir( $dir ) { return Data_Manager::get_data_dir( $dir ); } @@ -134,24 +267,52 @@ if ( ! function_exists( 'upserv_get_root_data_dir' ) ) { } if ( ! function_exists( 'upserv_get_packages_data_dir' ) ) { + /** + * Get the path to the packages directory on the file system. + * + * @since 1.0 + * + * @return string The path to the packages directory on the file system + */ function upserv_get_packages_data_dir() { return Data_Manager::get_data_dir( 'packages' ); } } if ( ! function_exists( 'upserv_get_logs_data_dir' ) ) { + /** + * Get the path to the plugin's log directory. + * + * @since 1.0 + * + * @return string The path to the plugin's log directory + */ function upserv_get_logs_data_dir() { return Data_Manager::get_data_dir( 'logs' ); } } if ( ! function_exists( 'upserv_get_cache_data_dir' ) ) { + /** + * Get the path to the plugin's package cache directory. + * + * @since 1.0 + * + * @return string The path to the plugin's package cache directory + */ function upserv_get_cache_data_dir() { return Data_Manager::get_data_dir( 'cache' ); } } if ( ! function_exists( 'upserv_get_package_metadata_data_dir' ) ) { + /** + * Get the path to the plugin's package metadata directory. + * + * @since 1.0 + * + * @return string The path to the plugin's package metadata directory + */ function upserv_get_package_metadata_data_dir() { return Data_Manager::get_data_dir( 'metadata' ); } @@ -162,18 +323,42 @@ if ( ! function_exists( 'upserv_get_package_metadata_data_dir' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_is_package_whitelisted' ) ) { + /** + * Determine whether a package is whitelisted. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @return bool `true` if the package is whitelisted, `false` otherwise + */ function upserv_is_package_whitelisted( $package_slug ) { return Package_Manager::get_instance()->is_package_whitelisted( $package_slug ); } } if ( ! function_exists( 'upserv_whitelist_package' ) ) { + /** + * Whitelist a package. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @return bool `true` if the package was successfully whitelisted, `false` otherwise + */ function upserv_whitelist_package( $package_slug ) { return Package_Manager::get_instance()->whitelist_package( $package_slug ); } } if ( ! function_exists( 'upserv_unwhitelist_package' ) ) { + /** + * Unwhitelist a package. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @return bool `true` if the package was successfully unwhitelisted, `false` otherwise + */ function upserv_unwhitelist_package( $package_slug ) { return Package_Manager::get_instance()->unwhitelist_package( $package_slug ); } @@ -184,6 +369,15 @@ if ( ! function_exists( 'upserv_unwhitelist_package' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_get_package_metadata' ) ) { + /** + * Get metadata of a package. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @param bool $json_encode Whether to return a JSON object (default) or a PHP associative array + * @return mixed The package metadata + */ function upserv_get_package_metadata( $package_slug, $json_encode = false ) { return Package_Manager::get_instance()->get_package_metadata( $package_slug, @@ -193,6 +387,15 @@ if ( ! function_exists( 'upserv_get_package_metadata' ) ) { } if ( ! function_exists( 'upserv_set_package_metadata' ) ) { + /** + * Set metadata of a package. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @param array $metadata The metadata to set + * @return bool `true` if the metadata was successfully set, `false` otherwise + */ function upserv_set_package_metadata( $package_slug, $metadata ) { return Package_Manager::get_instance()->set_package_metadata( $package_slug, @@ -206,18 +409,39 @@ if ( ! function_exists( 'upserv_set_package_metadata' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_force_cleanup_cache' ) ) { + /** + * Force clean up the `cache` plugin data. + * + * @since 1.0 + * + * @return bool `true` in case of success, `false` otherwise + */ function upserv_force_cleanup_cache() { return Data_Manager::maybe_cleanup( 'cache', true ); } } if ( ! function_exists( 'upserv_force_cleanup_logs' ) ) { + /** + * Force clean up the `logs` plugin data. + * + * @since 1.0 + * + * @return bool `true` in case of success, `false` otherwise + */ function upserv_force_cleanup_logs() { return Data_Manager::maybe_cleanup( 'logs', true ); } } if ( ! function_exists( 'upserv_force_cleanup_tmp' ) ) { + /** + * Force clean up the `tmp` plugin data. + * + * @since 1.0 + * + * @return bool `true` in case of success, `false` otherwise + */ function upserv_force_cleanup_tmp() { return Data_Manager::maybe_cleanup( 'tmp', true ); } @@ -228,18 +452,43 @@ if ( ! function_exists( 'upserv_force_cleanup_tmp' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_check_remote_plugin_update' ) ) { + /** + * Determine whether the remote plugin package is an updated version compared to one on the file system. + * + * @since 1.0 + * + * @param string $slug The slug of the plugin package to check + * @return bool `true` if the remote plugin package is an updated version, `false` otherwise. If the local package does not exist, returns `true` + */ function upserv_check_remote_plugin_update( $slug ) { return upserv_check_remote_package_update( $slug, 'plugin' ); } } if ( ! function_exists( 'upserv_check_remote_theme_update' ) ) { + /** + * Determine whether the remote theme package is an updated version compared to the one on the file system. + * + * @since 1.0 + * + * @param string $slug The slug of the theme package to check + * @return bool `true` if the remote theme package is an updated version, `false` otherwise. If the package does not exist on the file system, returns `true` + */ function upserv_check_remote_theme_update( $slug ) { return upserv_check_remote_package_update( $slug, 'theme' ); } } if ( ! function_exists( 'upserv_check_remote_package_update' ) ) { + /** + * Determine whether the remote package is an updated version compared to the one on the file system. + * + * @since 1.0 + * + * @param string $slug The slug of the package to check + * @param string $type The type of the package + * @return bool `true` if the remote package is an updated version, `false` otherwise. If the local package does not exist, returns `true` + */ function upserv_check_remote_package_update( $slug, $type ) { $api = Update_API::get_instance(); @@ -248,18 +497,52 @@ if ( ! function_exists( 'upserv_check_remote_package_update' ) ) { } if ( ! function_exists( 'upserv_download_remote_plugin' ) ) { + /** + * Download a plugin package from the Version Control System to the package directory on the file system. + * If `$vcs_url` and `$branch` are provided, the plugin will attempt to get an existing VCS configuration and register the package with it. + * + * @since 1.0 + * + * @param string $slug The slug of the plugin package to download + * @param string $vcs_url The URL of a VCS configured in UpdatePulse Server; default to `false` + * @param string $branch The branch as provided in a VCS configured in UpdatePulse Server; default to `'main'` + * @return bool `true` if the plugin package was successfully downloaded, `false` otherwise + */ function upserv_download_remote_plugin( $slug, $vcs_url = false, $branch = 'main' ) { return upserv_download_remote_package( $slug, 'plugin', $vcs_url, $branch ); } } if ( ! function_exists( 'upserv_download_remote_theme' ) ) { + /** + * Download a theme package from the Version Control System to the package directory on the file system. + * If `$vcs_url` and `$branch` are provided, the plugin will attempt to get an existing VCS configuration and register the package with it. + * + * @since 1.0 + * + * @param string $slug The slug of the theme package to download + * @param string $vcs_url The URL of a VCS configured in UpdatePulse Server; default to `false` + * @param string $branch The branch as provided in a VCS configured in UpdatePulse Server; default to `'main'` + * @return bool `true` if the theme package was successfully downloaded, `false` otherwise + */ function upserv_download_remote_theme( $slug, $vcs_url = false, $branch = 'main' ) { return upserv_download_remote_package( $slug, 'theme', $vcs_url, $branch ); } } if ( ! function_exists( 'upserv_download_remote_package' ) ) { + /** + * Download a package from the Version Control System to the package directory on the file system. + * If `$vcs_url` and `$branch` are provided, the plugin will attempt to get an existing VCS configuration and register the package with it. + * + * @since 1.0 + * + * @param string $slug The slug of the package to download + * @param string $type The type of the package; default to `'generic'` + * @param string $vcs_url The URL of a VCS configured in UpdatePulse Server; default to `false` + * @param string $branch The branch as provided in a VCS configured in UpdatePulse Server; default to `'main'` + * @return bool|WP_Error `WP_Error` if provided VCS information is invalid, `true` if the package was successfully downloaded, `false` otherwise + */ function upserv_download_remote_package( $slug, $type = 'generic', $vcs_url = false, $branch = 'main' ) { if ( $vcs_url ) { @@ -286,6 +569,14 @@ if ( ! function_exists( 'upserv_download_remote_package' ) ) { } if ( ! function_exists( 'upserv_get_package_vcs_config' ) ) { + /** + * Get the Version Control System (VCS) configuration for a package. + * + * @since 1.0 + * + * @param string $slug The slug of the package + * @return array The VCS configuration for the package + */ function upserv_get_package_vcs_config( $slug ) { $meta = upserv_get_package_metadata( $slug ); @@ -298,6 +589,14 @@ if ( ! function_exists( 'upserv_get_package_vcs_config' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_delete_package' ) ) { + /** + * Delete a package on the file system. + * + * @since 1.0 + * + * @param string $slug The slug of the package to delete + * @return bool `true` if the package was successfully deleted, `false` otherwise + */ function upserv_delete_package( $slug ) { $package_manager = Package_Manager::get_instance(); @@ -306,6 +605,15 @@ if ( ! function_exists( 'upserv_delete_package' ) ) { } if ( ! function_exists( 'upserv_get_package_info' ) ) { + /** + * Get information about a package on the file system. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @param bool $json_encode Whether to return a JSON object (default) or a PHP associative array + * @return mixed The package information as a PHP associative array or a JSON object + */ function upserv_get_package_info( $package_slug, $json_encode = true ) { $result = $json_encode ? '{}' : array(); $package_manager = Package_Manager::get_instance(); @@ -320,6 +628,14 @@ if ( ! function_exists( 'upserv_get_package_info' ) ) { } if ( ! function_exists( 'upserv_is_package_require_license' ) ) { + /** + * Determine whether a package requires a license key. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @return bool `true` if the package requires a license key, `false` otherwise + */ function upserv_is_package_require_license( $package_slug ) { $api = License_API::get_instance(); @@ -328,6 +644,15 @@ if ( ! function_exists( 'upserv_is_package_require_license' ) ) { } if ( ! function_exists( 'upserv_get_batch_package_info' ) ) { + /** + * Get batch information of packages on the file system. + * + * @since 1.0 + * + * @param string $search Search string to be used in package's slug and package's name (case insensitive) + * @param bool $json_encode Whether to return a JSON object (default) or a PHP associative array + * @return mixed The batch information as a PHP associative array or a JSON object; each entry is formatted like in `upserv_get_package_info` + */ function upserv_get_batch_package_info( $search, $json_encode = true ) { $result = $json_encode ? '{}' : array(); $package_manager = Package_Manager::get_instance(); @@ -342,6 +667,16 @@ if ( ! function_exists( 'upserv_get_batch_package_info' ) ) { } if ( ! function_exists( 'upserv_download_local_package' ) ) { + /** + * Start a download of a package from the file system and exits. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @param string $package_path The path of the package on the local file system - if `null`, will attempt to find it using `upserv_get_local_package_path( $package_slug )`; default `null` + * @param bool $exit_or_die Whether to exit or die after the download; default `true` + * @return void + */ function upserv_download_local_package( $package_slug, $package_path = null, $exit_or_die = true ) { $package_manager = Package_Manager::get_instance(); @@ -354,6 +689,14 @@ if ( ! function_exists( 'upserv_download_local_package' ) ) { } if ( ! function_exists( 'upserv_get_local_package_path' ) ) { + /** + * Get the path of a plugin, theme, or generic package on the file system. + * + * @since 1.0 + * + * @param string $package_slug The slug of the package + * @return string|false The path of the package on the local file system or `false` if it does not exist + */ function upserv_get_local_package_path( $package_slug ) { WP_Filesystem(); @@ -376,8 +719,16 @@ if ( ! function_exists( 'upserv_get_local_package_path' ) ) { /******************************************************************* * Licenses functions *******************************************************************/ - if ( ! function_exists( 'upserv_browse_licenses' ) ) { + /** + * Browse the license records filtered using various criteria. + * + * @since 1.0 + * + * @param array $license_query The License Query + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#the-license-query + * @return array An array of license objects matching the License Query. + */ function upserv_browse_licenses( $license_query ) { $api = License_API::get_instance(); @@ -386,6 +737,16 @@ if ( ! function_exists( 'upserv_browse_licenses' ) ) { } if ( ! function_exists( 'upserv_read_license' ) ) { + /** + * Read a license record. + * + * @since 1.0 + * + * @param array $license_data The License payload data. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#read + * @return mixed An object in case of success or an empty array otherwise. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#read the object is the decoded value of the JSON string + */ function upserv_read_license( $license_data ) { $api = License_API::get_instance(); @@ -394,6 +755,16 @@ if ( ! function_exists( 'upserv_read_license' ) ) { } if ( ! function_exists( 'upserv_add_license' ) ) { + /** + * Add a license. + * + * @since 1.0 + * + * @param array $license_data The License payload data + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#add + * @return mixed An object in case of success or an array of errors otherwise. + * * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#add the object is the decoded value of the JSON string + */ function upserv_add_license( $license_data ) { if ( is_array( $license_data ) && ! isset( $license_data['data'] ) ) { @@ -410,6 +781,16 @@ if ( ! function_exists( 'upserv_add_license' ) ) { } if ( ! function_exists( 'upserv_edit_license' ) ) { + /** + * Edit a license record. + * + * @since 1.0 + * + * @param array $license_data The License payload data. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#edit + * @return mixed An object in case of success or an array of errors otherwise. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#edit the object is the decoded value of the JSON string + */ function upserv_edit_license( $license_data ) { if ( is_array( $license_data ) && ! isset( $license_data['data'] ) ) { @@ -426,6 +807,16 @@ if ( ! function_exists( 'upserv_edit_license' ) ) { } if ( ! function_exists( 'upserv_delete_license' ) ) { + /** + * Delete a license record. + * + * @since 1.0 + * + * @param array $license_data The License payload data. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#delete + * @return mixed An object in case of success or an empty array otherwise. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#delete the object is the decoded value of the JSON string + */ function upserv_delete_license( $license_data ) { if ( is_array( $license_data ) && ! isset( $license_data['data'] ) ) { @@ -442,6 +833,16 @@ if ( ! function_exists( 'upserv_delete_license' ) ) { } if ( ! function_exists( 'upserv_check_license' ) ) { + /** + * Check a License information. + * + * @since 1.0 + * + * @param array $license_data An associative array with a single value - `array( 'license_key' => 'key_of_the_license_to_check' )`. + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#check + * @return mixed An object in case of success, and associative array in case of failure + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#check the object is the decoded value of the JSON string + */ function upserv_check_license( $license_data ) { $api = License_API::get_instance(); @@ -450,6 +851,15 @@ if ( ! function_exists( 'upserv_check_license' ) ) { } if ( ! function_exists( 'upserv_activate_license' ) ) { + /** + * Activate a License. + * + * @since 1.0 + * + * @param array $license_data An associative array with 2 values - `array( 'license_key' => 'key_of_the_license_to_activate', 'allowed_domains' => 'domain_to_activate' )`. + * @return mixed An object in case of success, and associative array in case of failure + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#activate the object is the decoded value of the JSON string + */ function upserv_activate_license( $license_data ) { $api = License_API::get_instance(); @@ -458,6 +868,15 @@ if ( ! function_exists( 'upserv_activate_license' ) ) { } if ( ! function_exists( 'upserv_deactivate_license' ) ) { + /** + * Deactivate a License. + * + * @since 1.0 + * + * @param array $license_data An associative array with 2 values - `array( 'license_key' => 'key_of_the_license_to_deactivate', 'allowed_domains' => 'domain_to_deactivate' )`. + * @return mixed An object in case of success, and associative array in case of failure + * @see https://github.com/Anyape/updatepulse-server/blob/main/docs/licenses.md#deactivate the object is the decoded value of the JSON string + */ function upserv_deactivate_license( $license_data ) { $api = License_API::get_instance(); @@ -470,6 +889,21 @@ if ( ! function_exists( 'upserv_deactivate_license' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_get_template' ) ) { + /** + * Loads a template file from the plugin's template directory + * + * This function locates and loads template files for the frontend of the plugin. + * It applies filters to the template name and arguments, sets up query variables, + * and then passes the template to the UPServ template loader. + * + * @since 1.0 + * + * @param string $template_name The name of the template to load + * @param array $args Arguments to pass to the template + * @param boolean $load Whether to load the template file + * @param boolean $require_file Whether to require or require_once the template file + * @return string|bool Path to the template file or false if not found + */ function upserv_get_template( $template_name, $args = array(), $load = true, $require_file = false ) { $template_name = apply_filters( 'upserv_get_template_name', $template_name, $args ); $template_args = apply_filters( 'upserv_get_template_args', $args, $template_name ); @@ -488,6 +922,21 @@ if ( ! function_exists( 'upserv_get_template' ) ) { } if ( ! function_exists( 'upserv_get_admin_template' ) ) { + /** + * Loads a template file from the plugin's admin template directory + * + * This function locates and loads template files for the admin area of the plugin. + * It applies filters to the template name and arguments, sets up query variables, + * and then passes the template to the UPServ admin template loader. + * + * @since 1.0 + * + * @param string $template_name The name of the admin template to load + * @param array $args Arguments to pass to the template + * @param boolean $load Whether to load the template file + * @param boolean $require_file Whether to require or require_once the template file + * @return string|bool Path to the template file or false if not found + */ function upserv_get_admin_template( $template_name, $args = array(), $load = true, $require_file = false ) { $template_name = apply_filters( 'upserv_get_admin_template_name', $template_name, $args ); $template_args = apply_filters( 'upserv_get_admin_template_args', $args, $template_name ); @@ -510,12 +959,33 @@ if ( ! function_exists( 'upserv_get_admin_template' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_init_nonce_auth' ) ) { + /** + * Initialize the nonce authentication. + * + * @since 1.0 + * + * @param string $private_auth_key The private authentication key + */ function upserv_init_nonce_auth( $private_auth_key ) { Nonce::init_auth( $private_auth_key ); } } if ( ! function_exists( 'upserv_create_nonce' ) ) { + /** + * Create a nonce + * + * Creates a cryptographic token - allows creation of tokens that are true one-time-use nonces, with custom expiry length and custom associated data. + * + * @since 1.0 + * + * @param bool $true_nonce Whether the nonce is one-time-use; default `true` + * @param int $expiry_length The number of seconds after which the nonce expires; default `UPServ_Nonce::DEFAULT_EXPIRY_LENGTH` - 30 seconds + * @param array $data Custom data to save along with the nonce; set an element with key `permanent` to a truthy value to create a nonce that never expires; default `array()` + * @param int $return_type Whether to return the nonce, or an array of information; default `UPServ_Nonce::NONCE_ONLY`; other accepted value is `UPServ_Nonce::NONCE_INFO_ARRAY` + * @param bool $store Whether to store the nonce, or let a third party mechanism take care of it; default `true` + * @return bool|string|array `false` in case of failure; the cryptographic token string if `$return_type` is set to `UPServ_Nonce::NONCE_ONLY`; an array of information if `$return_type` is set to `UPServ_Nonce::NONCE_INFO_ARRAY` + */ function upserv_create_nonce( $true_nonce = true, $expiry_length = Nonce::DEFAULT_EXPIRY_LENGTH, @@ -528,36 +998,86 @@ if ( ! function_exists( 'upserv_create_nonce' ) ) { } if ( ! function_exists( 'upserv_get_nonce_expiry' ) ) { + /** + * Get the expiry timestamp of a nonce. + * + * @since 1.0 + * + * @param string $nonce The nonce + * @return int The expiry timestamp + */ function upserv_get_nonce_expiry( $nonce ) { return Nonce::get_nonce_expiry( $nonce ); } } if ( ! function_exists( 'upserv_get_nonce_data' ) ) { + /** + * Get the data stored along a nonce. + * + * @since 1.0 + * + * @param string $nonce The nonce + * @return array The data stored along the nonce + */ function upserv_get_nonce_data( $nonce ) { return Nonce::get_nonce_data( $nonce ); } } if ( ! function_exists( 'upserv_validate_nonce' ) ) { + /** + * Check whether the value is a valid nonce. + * + * @since 1.0 + * + * @param string $value The value to check + * @return bool Whether the value is a valid nonce + */ function upserv_validate_nonce( $value ) { return Nonce::validate_nonce( $value ); } } if ( ! function_exists( 'upserv_delete_nonce' ) ) { + /** + * Delete a nonce from the system if the corresponding value exists. + * + * @since 1.0 + * + * @param string $value The value to delete + * @return bool Whether the nonce was deleted + */ function upserv_delete_nonce( $value ) { return Nonce::delete_nonce( $value ); } } if ( ! function_exists( 'upserv_clear_nonces' ) ) { + /** + * Clear expired nonces from the system. + * + * @since 1.0 + * + * @return bool Whether some nonces were cleared + */ function upserv_clear_nonces() { return Nonce::upserv_nonce_cleanup(); } } if ( ! function_exists( 'upserv_build_nonce_api_signature' ) ) { + /** + * Build credentials and signature for UpdatePulse Server Nonce API. + * + * @since 1.0 + * + * @param string $api_key_id The ID of the Private API Key + * @param string $api_key The Private API Key - will not be sent over the Internet + * @param int $timestamp The timestamp used to limit the validity of the signature (validity is MINUTE_IN_SECONDS) + * @param int $payload The payload to acquire a reusable token or a true nonce + * @return array An array with keys `credentials` and `signature` + */ function upserv_build_nonce_api_signature( $api_key_id, $api_key, $timestamp, $payload ) { unset( $payload['api_signature'] ); unset( $payload['api_credentials'] ); @@ -595,6 +1115,16 @@ if ( ! function_exists( 'upserv_build_nonce_api_signature' ) ) { *******************************************************************/ if ( ! function_exists( 'upserv_schedule_webhook' ) ) { + /** + * Schedule an event notification to be sent to registered Webhook URLs at next cron run. + * + * @since 1.0 + * + * @param array $payload The data used to schedule the notification + * @param string $event_type The type of event; the payload will only be delivered to URLs subscribed to this type + * @param boolean $instant Whether to send the notification immediately; default `false` + * @return null|WP_Error `null` in case of success, a `WP_Error` otherwise + */ function upserv_schedule_webhook( $payload, $event_type, $instant = false ) { if ( isset( $payload['event'], $payload['content'] ) ) { @@ -611,6 +1141,17 @@ if ( ! function_exists( 'upserv_schedule_webhook' ) ) { } if ( ! function_exists( 'upserv_fire_webhook' ) ) { + /** + * Immediately send a event notification to `$url`, signed with `$secret` with resulting hash stored in `X-UpdatePulse-Signature-256`, with `$action` in `X-UpdatePulse-Action`. + * + * @since 1.0 + * + * @param string $url The destination of the notification + * @param string $secret The secret used to sign the notification + * @param string $body The JSON string sent in the notification + * @param string $action The WordPress action responsible for firing the webhook + * @return array|WP_Error The response of the request in case of success, a `WP_Error` otherwise + */ function upserv_fire_webhook( $url, $secret, $body, $action ) { if ( diff --git a/lib/anyape-crypto/crypto.php b/lib/anyape-crypto/crypto.php index dab2ff0..a713e9e 100644 --- a/lib/anyape-crypto/crypto.php +++ b/lib/anyape-crypto/crypto.php @@ -4,11 +4,33 @@ namespace Anyape\Crypto; use Exception; +/** + * Cryptography utility class for encryption, decryption, and HMAC operations. + * + * This class provides methods for secure data encryption and authentication + * using AES-256-CBC encryption and HMAC-SHA256 signatures. + */ class Crypto { - const METHOD = 'aes-256-cbc'; + /** + * The encryption cipher method used for all encryption operations. + */ + const METHOD = 'aes-256-cbc'; + + /** + * Character used to replace forward slashes in base64url encoding. + */ const SLASH_REPLACE = '_'; + /** + * Encrypts a message using AES-256-CBC with HMAC authentication. + * + * @param string $message The plaintext message to encrypt. + * @param string $crypt_key The encryption key. + * @param string $sign_key The signing key for HMAC authentication. + * @return string|false The encrypted, authenticated, and encoded string, or false on failure. + * @throws Exception When the key length is invalid. + */ public static function encrypt( $message, $crypt_key, $sign_key ) { $cipher = false; $crypt_key = hex2bin( hash( 'sha256', $crypt_key ) ); @@ -29,6 +51,15 @@ class Crypto { return $cipher; } + /** + * Decrypts an encrypted message after verifying its HMAC signature. + * + * @param string $cipher The encrypted, authenticated, and encoded string. + * @param string $crypt_key The encryption key. + * @param string $sign_key The signing key for HMAC verification. + * @return string|false The decrypted message, or false if verification fails. + * @throws Exception When the key length is invalid. + */ public static function decrypt( $cipher, $crypt_key, $sign_key ) { $message = false; $crypt_key = hex2bin( hash( 'sha256', $crypt_key ) ); @@ -51,12 +82,26 @@ class Crypto { return $message; } + /** + * Signs a message using HMAC-SHA256. + * + * @param string $message The message to sign. + * @param string $sign_key The signing key. + * @return string The HMAC signature. + */ public static function hmac_sign( $message, $sign_key ) { $signature = hash_hmac( 'sha256', $message, $sign_key, true ); return $signature; } + /** + * Verifies an HMAC signature. + * + * @param string $original_val The original HMAC signature. + * @param string $new_val The new HMAC signature to compare. + * @return bool True if the signatures match, false otherwise. + */ public static function hmac_verify( $original_val, $new_val ) { if ( function_exists( 'hash_equals' ) ) { @@ -82,10 +127,22 @@ class Crypto { return 0 === $result; } + /** + * Encodes data using base64url encoding. + * + * @param string $s The data to encode. + * @return string The base64url encoded string. + */ public static function base64url_encode( $s ) { return str_replace( '/', self::SLASH_REPLACE, base64_encode( $s ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } + /** + * Decodes a base64url encoded string. + * + * @param string $s The base64url encoded string. + * @return string The decoded data. + */ public static function base64url_decode( $s ) { return base64_decode( str_replace( self::SLASH_REPLACE, '/', $s ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode } diff --git a/lib/anyape-package-parser/package-parser.php b/lib/anyape-package-parser/package-parser.php index ce1e97a..6b272b9 100644 --- a/lib/anyape-package-parser/package-parser.php +++ b/lib/anyape-package-parser/package-parser.php @@ -7,25 +7,30 @@ use ZipArchive as SystemZipArchive; // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound +/** + * Package parser for WordPress plugins and themes. + * + * This class provides functionality to extract and analyze information from WordPress plugin and theme packages, or generic packages, in ZIP format. + */ class Parser { /** - * Extract headers and readme.txt data from a ZIP archive that contains a package. + * Extracts and parses metadata from a WordPress plugin or theme ZIP package. * - * Returns an associative array with these keys: - * 'type' - Detected package type. This can be either "plugin" or "theme". - * 'header' - An array of package headers. See get_plugin_data() or WP_Theme for details. - * 'readme' - An array of metadata extracted from readme.txt. @see self::parseReadme() - * 'pluginFile' - The name of the PHP file where the plugin headers were found relative to the root directory of the ZIP archive. - * 'stylesheet' - The relative path to the style.css file that contains theme headers, if any. + * Analyzes the contents of a ZIP archive to determine if it contains a valid WordPress plugin, theme, or generic package, then extracts relevant metadata from header files and readme.txt (if present). * - * The 'readme' key will only be present if the input archive contains a readme.txt file - * formatted according to WordPress.org readme standards. Similarly, 'pluginFile' and - * 'stylesheet' will only be present if the archive contains a plugin or a theme, respectively. + * The function returns an array with the following structure: + * 'type' - Package type: "plugin", "theme", or "generic" + * 'header' - Package header information (varies by type) + * 'readme' - Metadata extracted from readme.txt (if available) + * 'pluginFile' - Path to the main plugin file (for plugins only) + * 'stylesheet' - Path to style.css file (for themes only) + * 'generic_file' - Path to updatepulse.json (for generic packages only) + * 'extra' - Additional metadata like icons and banners (if available) * * @param string $package_filename The path to the ZIP package. * @param bool $apply_markdown Whether to transform markup used in readme.txt to HTML. Defaults to false. - * @return array|bool Either an associative array or FALSE if the input file is not a valid ZIP archive or doesn't contain a package. + * @return array|bool Package information array or FALSE if invalid/unreadable. */ public static function parse_package( $package_filename, $apply_markdown = false ) { @@ -39,7 +44,7 @@ class Parser { return false; } - //Find and parse the package file and ( optionally ) readme.txt. + // Find and parse the package file and (optionally) readme.txt $header = null; $readme = null; $plugin_file = null; @@ -57,27 +62,36 @@ class Parser { $file_index++ ) { $info = $entries[ $file_index ]; - //Normalize filename: convert backslashes to slashes, remove leading slashes. - $file_name = trim( str_replace( '\\', '/', $info['name'] ), '/' ); - $file_name = ltrim( $file_name, '/' ); + // Normalize filename: convert backslashes to slashes, remove leading slashes + $file_name = trim( str_replace( '\\', '/', $info['name'] ), '/' ); + $file_name = ltrim( $file_name, '/' ); + + // Add path traversal protection + if ( false !== strpos( $file_name, '../' ) || false !== strpos( $file_name, '..\\' ) ) { + // Log attempt and skip this file + error_log( __METHOD__ . ' Potential path traversal attempt blocked for file: ' . $file_name ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + + continue; + } + $file_name_parts = explode( '.', $file_name ); $extension = strtolower( end( $file_name_parts ) ); $depth = substr_count( $file_name, '/' ); - // Skip empty files, directories and everything that's more than 1 sub-directory deep. + // Skip files that are either empty, directories, or nested deeper than one level if ( ( $depth > 1 ) || $info['isFolder'] ) { continue; } - // readme.txt ( for plugins )? + // Check for and parse readme.txt file for plugins if ( empty( $readme ) && ( strtolower( basename( $file_name ) ) === 'readme.txt' ) ) { - //Try to parse the readme. + // Attempt to parse the readme content $readme = self::parse_readme( $zip->get_file_contents( $info ), $apply_markdown ); } $file_contents = null; - // Theme stylesheet? + // Check if the provided file is for a theme if ( empty( $header ) && ( strtolower( basename( $file_name ) ) === 'style.css' ) ) { $file_contents = substr( $zip->get_file_contents( $info ), 0, 8 * 1024 ); $header = self::get_theme_headers( $file_contents ); @@ -89,7 +103,7 @@ class Parser { } } - // Main plugin file? + // Check if the provided file is for a plugin if ( empty( $header ) && ( 'php' === $extension ) ) { $file_contents = substr( $zip->get_file_contents( $info ), 0, 8 * 1024 ); $plugin_file = $file_name; @@ -98,7 +112,7 @@ class Parser { $type = 'plugin'; } - // Generic info file? + // Check if the provided file is a generic package if ( empty( $header ) && ( 'json' === $extension ) && ( basename( $file_name ) === 'updatepulse.json' ) ) { $file_contents = substr( $zip->get_file_contents( $info ), 0, 8 * 1024 ); $header = self::get_generic_headers( $file_contents ); @@ -121,29 +135,25 @@ class Parser { } /** - * Parse a plugin's readme.txt to extract various plugin metadata. + * Extracts metadata from a WordPress plugin/theme readme.txt file. * - * Returns an array with the following fields: - * 'name' - Name of the plugin. - * 'contributors' - An array of wordpress.org usernames. - * 'donate' - The plugin's donation link. - * 'tags' - An array of the plugin's tags. - * 'requires' - The minimum version of WordPress that the plugin will run on. - * 'tested' - The latest version of WordPress that the plugin has been tested on. - * 'stable' - The SVN tag of the latest stable release, or 'trunk'. - * 'short_description' - The plugin's "short description". - * 'sections' - An associative array of sections present in the readme.txt. - * Case and formatting of section headers will be preserved. + * Parses the standardized WordPress readme.txt format to extract key information about the plugin or theme, including version requirements, descriptions, and documentation sections. * - * Be warned that this function does *not* perfectly emulate the way that WordPress.org - * parses plugin readme's. In particular, it may mangle certain HTML markup that wp.org - * handles correctly. + * The returned array includes: + * 'name' - Plugin/theme name + * 'contributors' - List of WordPress.org contributor usernames + * 'donate' - Donation URL + * 'tags' - Plugin tags/categories + * 'requires' - Minimum WordPress version + * 'requires_php' - Minimum PHP version + * 'tested' - WordPress version tested up to + * 'stable' - Stable release tag + * 'short_description' - Brief plugin description + * 'sections' - Content sections (FAQ, installation, etc.) * - * @see http://wordpress.org/extend/plugins/about/readme.txt - * - * @param string $readme_txt_contents The contents of a plugin's readme.txt file. - * @param bool $apply_markdown Whether to transform Markdown used in readme.txt sections to HTML. Defaults to false. - * @return array|null Associative array, or NULL if the input isn't a valid readme.txt file. + * @param string $readme_txt_contents The contents of a readme.txt file. + * @param bool $apply_markdown Whether to convert Markdown to HTML. Defaults to false. + * @return array|null Parsed readme data or NULL if invalid format. */ public static function parse_readme( $readme_txt_contents, $apply_markdown = false ) { $readme_txt_contents = trim( $readme_txt_contents, " \t\n\r" ); @@ -160,17 +170,17 @@ class Parser { 'sections' => array(), ); - //The readme.txt header has a fairly fixed structure, so we can parse it line-by-line + // Do a line-by-line parse of the readme.txt file $lines = explode( "\n", $readme_txt_contents ); - //Plugin name is at the very top, e.g. === My Plugin === + // Get the name of the plugin if ( preg_match( '@===\s*( .+? )\s*===@', array_shift( $lines ), $matches ) ) { $readme['name'] = $matches[1]; } else { return null; } - //Then there's a bunch of meta fields formatted as "Field: value" + // Set up a map of header fields to their corresponding keys in the readme array $headers = array(); $header_map = array( 'Contributors' => 'contributors', @@ -182,7 +192,7 @@ class Parser { 'Stable tag' => 'stable', ); - do { //Parse each readme.txt header + do { $pieces = explode( ':', array_shift( $lines ), 2 ); if ( array_key_exists( $pieces[0], $header_map ) ) { @@ -193,53 +203,53 @@ class Parser { $headers[ $header_map[ $pieces[0] ] ] = ''; } } - } while ( trim( $pieces[0] ) !== '' ); //Until an empty line is encountered + } while ( trim( $pieces[0] ) !== '' ); - //"Contributors" is a comma-separated list. Convert it to an array. + // Convert comma-separated contributors list into an array if ( ! empty( $headers['contributors'] ) ) { $headers['contributors'] = array_map( 'trim', explode( ',', $headers['contributors'] ) ); } - //Likewise for "Tags" + // Convert comma-separated tags list into an array if ( ! empty( $headers['tags'] ) ) { $headers['tags'] = array_map( 'trim', explode( ',', $headers['tags'] ) ); } $readme = array_merge( $readme, $headers ); - //After the headers comes the short description + // Extract the short description from the next line $readme['short_description'] = array_shift( $lines ); - //Finally, a valid readme.txt also contains one or more "sections" identified by "== Section Name ==" + // Parse remaining content into sections (e.g., "== Description ==", "== Installation ==", etc.) $sections = array(); $content_buffer = array(); $current_section = ''; foreach ( $lines as $line ) { - //Is this a section header? + // Check if there is a section header if ( preg_match( '@^\s*==\s+(.+?)\s+==\s*$@m', $line, $matches ) ) { - //Flush the content buffer for the previous section, if any + // Flush the content buffer for the previous section, if any if ( ! empty( $current_section ) ) { $section_content = trim( implode( "\n", $content_buffer ) ); $sections[ $current_section ] = $section_content; } - //Start reading a new section + // Read a new section $current_section = $matches[1]; $content_buffer = array(); } else { - //Buffer all section content + // Buffer all section content $content_buffer[] = $line; } } - //Flush the buffer for the last section + // Flush the buffer for the last section if ( ! empty( $current_section ) ) { $sections[ $current_section ] = trim( implode( "\n", $content_buffer ) ); } - //Apply Markdown to sections + // Apply Markdown to sections if ( $apply_markdown ) { $sections = array_map( __CLASS__ . '::apply_markdown', $sections ); } @@ -250,41 +260,31 @@ class Parser { } /** - * Transform Markdown markup to HTML. + * Converts Markdown syntax to HTML format. * - * Tries ( in vain ) to emulate the transformation that WordPress.org applies to readme.txt files. + * This method processes text with Markdown formatting and returns HTML content. + * It handles WordPress-specific readme.txt formatting conventions, including custom header syntax like "= H4 headers =". * - * @param string $text - * @return string + * @param string $text Text content with Markdown formatting + * @return string HTML-formatted content */ private static function apply_markdown( $text ) { - //The WP standard for readme files uses some custom markup, like "= H4 headers =" + // The WP standard for readme files uses some custom markup, like "= H4 headers =" $text = preg_replace( '@^\s*=\s*( .+? )\s*=\s*$@m', "\n####$1####\n", $text ); return Parsedown::instance()->text( $text ); } /** - * Parse the plugin contents to retrieve plugin's metadata headers. + * Extracts plugin header metadata from PHP file content. * - * Adapted from the get_plugin_data() function used by WordPress. - * Returns an array that contains the following: - * 'Name' - Name of the plugin. - * 'Title' - Title of the plugin and the link to the plugin's web site. - * 'Description' - Description of what the plugin does and/or notes from the author. - * 'Author' - The author's name. - * 'AuthorURI' - The author's web site address. - * 'Version' - The plugin version number. - * 'PluginURI' - Plugin web site address. - * 'TextDomain' - Plugin's text domain for localization. - * 'DomainPath' - Plugin's relative directory path to .mo files. - * 'Network' - Boolean. Whether the plugin can only be activated network wide. + * Parses a plugin file to extract standard WordPress plugin headers like name, version, author information, and other metadata. This mimics WordPress's get_plugin_data() function to handle plugin header extraction. * - * If the input string doesn't appear to contain a valid plugin header, the function - * will return NULL. + * The returned array includes: + * 'Name', 'Title', 'Description', 'Author', 'AuthorURI', 'Version', 'PluginURI', 'TextDomain', 'DomainPath', 'Network', 'Depends', 'Provides', 'RequiresPHP', and others. * * @param string $file_contents Contents of the plugin file - * @return array|null See above for description. + * @return array|null Plugin metadata or NULL if no valid plugin header found */ public static function get_plugin_headers( $file_contents ) { //[Internal name => Name used in the plugin file] @@ -308,7 +308,6 @@ class Parser { ) ); - //Site Wide Only is the old header for Network. if ( empty( $headers['Network'] ) && ! empty( $headers['_sitewide'] ) ) { $headers['Network'] = $headers['_sitewide']; } @@ -317,20 +316,20 @@ class Parser { $headers['Network'] = ( strtolower( $headers['Network'] ) === 'true' ); - //For backward compatibility by default Title is the same as Name. + // For backward compatibility, by default, Title is the same as Name. $headers['Title'] = $headers['Name']; - //"Depends" is a comma-separated list. Convert it to an array. + // Comma-separated list. Convert it to an array. if ( ! empty( $headers['Depends'] ) ) { $headers['Depends'] = array_map( 'trim', explode( ',', $headers['Depends'] ) ); } - //Same for "Provides" + // Comma-separated list. Convert it to an array. if ( ! empty( $headers['Provides'] ) ) { $headers['Provides'] = array_map( 'trim', explode( ',', $headers['Provides'] ) ); } - //If it doesn't have a name, it's probably not a plugin. + // If no name is found, return null - not a plugin. if ( empty( $headers['Name'] ) ) { return null; } @@ -339,27 +338,14 @@ class Parser { } /** - * Parse the theme stylesheet to retrieve its metadata headers. + * Extracts theme metadata from style.css file content. * - * Adapted from the get_theme_data() function and the WP_Theme class in WordPress. - * Returns an array that contains the following: - * 'Name' - Name of the theme. - * 'Description' - Theme description. - * 'Author' - The author's name - * 'AuthorURI' - The authors web site address. - * 'Version' - The theme version number. - * 'ThemeURI' - Theme web site address. - * 'Template' - The slug of the parent theme. Only applies to child themes. - * 'Status' - Unknown. Included for completeness. - * 'Tags' - An array of tags. - * 'TextDomain' - Theme's text domain for localization. - * 'DomainPath' - Theme's relative directory path to .mo files. + * Analyzes a WordPress theme's style.css file to extract standardized theme headers that provide information about the theme, including name, version, author details, and theme dependencies. * - * If the input string doesn't appear to contain a valid theme header, the function - * will return NULL. + * The returned array includes: 'Name', 'Description', 'Author', 'AuthorURI', 'Version', 'ThemeURI', 'Template' (parent theme), 'Tags', 'TextDomain', 'DomainPath', and more. * - * @param string $file_contents Contents of the theme stylesheet. - * @return array|null See above for description. + * @param string $file_contents Contents of the theme stylesheet + * @return array|null Theme metadata or NULL if no valid theme header found */ public static function get_theme_headers( $file_contents ) { //[Internal name => Name used in the theme file] @@ -383,7 +369,7 @@ class Parser { $headers['Tags'] = array_filter( array_map( 'trim', explode( ',', wp_strip_all_tags( $headers['Tags'] ) ) ) ); - //If it doesn't have a name, it's probably not a valid theme. + // If no name is found, return null - not a theme. if ( empty( $headers['Name'] ) ) { return null; } @@ -392,16 +378,15 @@ class Parser { } /** - * Parse the generic package's headers from updatepulse.json file. - * Returns an array that may contain the following: - * 'Name' - * 'Version' - * 'Homepage' - * 'Author' - * 'AuthorURI' - * 'Description' - * @param string $file_contents Contents of the package file - * @return array See above for description. + * Extracts generic package metadata from updatepulse.json file. + * + * Parses a JSON file containing package metadata for non-standard WordPress packages. This allows UpdatePulse to manage generic software packages alongside plugins and themes. + * + * The function extracts standard package information fields: + * 'Name', 'Version', 'Homepage', 'Author', 'AuthorURI', 'Description' + * + * @param string $file_contents Contents of the updatepulse.json file + * @return array Extracted package metadata */ public static function get_generic_headers( $file_contents ) { $decoded_contents = json_decode( $file_contents, true ); @@ -430,16 +415,18 @@ class Parser { } /** - * Parse the generic package's extra headers from updatepulse.json file. - * Returns an array that may contain the following: - * 'Icon1x' - * 'Icon2x' - * 'BannerHigh' - * 'BannerLow' - * 'RequireLicense' - * 'LicensedWith' - * @param string $file_contents Contents of the package file - * @return array See above for description. + * Extracts additional metadata from a generic package's JSON file. + * + * Parses the updatepulse.json file to retrieve supplementary informationlike icons, banners, and licensing requirements for generic packages. + * + * The returned array may contain: + * 'icons' - Package icons in different resolutions + * 'banners' - Banner images in high/low resolutions + * 'require_license' - Whether the package requires license validation + * 'licensed_with' - Associated licensing system or provider + * + * @param string $file_contents Contents of the updatepulse.json file + * @return array Additional package metadata */ public static function get_generic_extra_headers( $file_contents ) { $decoded_contents = json_decode( $file_contents, true ); @@ -489,23 +476,18 @@ class Parser { } /** - * Parse the package contents to retrieve icons and banners information. + * Extracts visual assets and licensing information from package files. * - * Returns an array that may contain the following: - * 'icons': - * 'Icon1x' - * 'Icon2x' - * 'banners': - * 'BannerHigh' - * 'BannerLow' - * 'Require License' - * 'Licensed With' + * Searches plugin and theme files for special headers that define supplementary assets like icons and banners, as well as licensing requirements. * - * If the data is not found, the function - * will return NULL. + * The returned array may include: + * 'icons' - Package icon URLs in different resolutions + * 'banners' - Banner image URLs in high/low resolutions + * 'require_license' - Whether the package requires license validation + * 'licensed_with' - Associated licensing system or provider * - * @param string $fileContents Contents of the package file - * @return array|null See above for description. + * @param string $file_contents Contents of a plugin or theme file + * @return array|null Supplementary metadata or NULL if none found */ public static function get_extra_headers( $file_contents ) { //[Internal name => Name used in the package file] @@ -562,19 +544,20 @@ class Parser { } /** - * Parse the file contents to retrieve its metadata. + * Extracts metadata headers from file contents. * - * Searches for metadata for a file, such as a package. Each piece of - * metadata must be on its own line. For a field spanning multiple lines, it - * must not have any newlines or only parts of it will be displayed. + * A low-level utility function that searches for formatted header comments in file content. It supports the standard WordPress header format used for plugins, themes, and other metadata files. * - * @param string $file_contents File contents. Can be safely truncated to 8kiB as that's all WP itself scans. - * @param array $header_map The list of headers to search for in the file. - * @return array + * Each header must appear on its own line in the format: + * "Header Name: Header Value" + * + * @param string $file_contents File content to search for headers + * @param array $header_map Map of internal header names to their file representation + * @return array Extracted header values indexed by internal names */ public static function get_file_headers( $file_contents, $header_map ) { $headers = array(); - //Support systems that use CR as a line ending. + // Support systems that use CR as a line ending. $file_contents = str_replace( "\r", "\n", $file_contents ); foreach ( $header_map as $field => $pretty_name ) { @@ -587,7 +570,7 @@ class Parser { ); if ( ( $found > 0 ) && ! empty( $matches[1] ) ) { - //Strip comment markers and closing PHP tags. + // Strip comment markers and closing PHP tags. $value = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $matches[1] ) ); $headers[ $field ] = $value; } else { @@ -599,6 +582,12 @@ class Parser { } } +/** + * Wrapper class for PHP's built-in ZipArchive. + * + * Provides a simplified interface for working with ZIP archives, + * specifically tailored for parsing WordPress package files. + */ class ZipArchive { /** * @var SystemZipArchive @@ -610,10 +599,13 @@ class ZipArchive { } /** - * Open a Zip archive. + * Opens a ZIP archive file for reading. * - * @param string $zip_file_name - * @return bool|ZipArchive + * Creates and initializes a ZipArchive instance from a file path. + * The method handles the low-level details of opening the archive. + * + * @param string $zip_file_name Path to the ZIP archive file + * @return bool|ZipArchive ZipArchive instance or FALSE on failure */ public static function open( $zip_file_name ) { $zip = new SystemZipArchive(); @@ -625,6 +617,13 @@ class ZipArchive { return new self( $zip ); } + /** + * Lists all entries in the ZIP archive. + * + * Provides information about each file and folder in the archive, including name, size, and whether it's a folder. + * + * @return array List of entry information arrays + */ public function list_entries() { $list = array(); $zip = $this->archive; @@ -645,6 +644,14 @@ class ZipArchive { return $list; } + /** + * Retrieves the contents of a file within the ZIP archive. + * + * Extracts and returns the contents of a specific file identified by its information array (typically from list_entries). + * + * @param array $file_info File information containing 'index' key + * @return string File contents + */ public function get_file_contents( $file_info ) { return $this->archive->getFromIndex( $file_info['index'] ); } diff --git a/lib/package-update-checker/Autoloader.php b/lib/package-update-checker/Autoloader.php index d69f31f..d707b44 100644 --- a/lib/package-update-checker/Autoloader.php +++ b/lib/package-update-checker/Autoloader.php @@ -4,13 +4,27 @@ namespace Anyape\PackageUpdateChecker; if ( ! class_exists( Autoloader::class, false ) ) : + /** + * Handles class autoloading for the Package Update Checker library. + * + * Automatically loads class files based on PSR-4 naming conventions when they are referenced in the code. + */ class Autoloader { + /** @var string The default namespace prefix for the library */ const DEFAULT_NS_PREFIX = 'Anyape\\PackageUpdateChecker\\'; + /** @var string The namespace prefix to handle */ private $prefix; + + /** @var string The root directory containing class files */ private $root_dir; + /** + * Initializes the autoloader and registers it with SPL. + * + * Sets up the root directory and namespace prefix, then registers the autoload method with PHP's SPL autoloader. + */ public function __construct() { $this->root_dir = __DIR__ . '/'; $namespace_with_slash = __NAMESPACE__ . '\\'; @@ -19,6 +33,14 @@ if ( ! class_exists( Autoloader::class, false ) ) : spl_autoload_register( array( $this, 'autoload' ) ); } + /** + * Attempts to load a class file based on its fully qualified name. + * + * Converts the namespace and class name into a file path and includes the file if it exists. + * + * @param string $class_name The fully qualified class name to load + * @return void + */ public function autoload( $class_name ) { if ( 0 === strpos( $class_name, $this->prefix ) ) { diff --git a/lib/package-update-checker/GenericUpdateChecker.php b/lib/package-update-checker/GenericUpdateChecker.php index 40be183..11d6444 100644 --- a/lib/package-update-checker/GenericUpdateChecker.php +++ b/lib/package-update-checker/GenericUpdateChecker.php @@ -4,8 +4,21 @@ namespace Anyape\PackageUpdateChecker; if ( ! class_exists( GenericUpdateChecker::class, false ) ) : + /** + * Generic update checker for non-WordPress packages. + * + * This class extends the base UpdateChecker to provide update checking functionality for generic packages that use JSON files for metadata storage. + */ class GenericUpdateChecker extends UpdateChecker { + /** + * Initializes a new instance of the generic update checker. + * + * @param object $api The Version Control System API instance + * @param string $slug The package's slug/directory name + * @param string $container The parent directory containing the package + * @param string $file_name The main package file name without extension + */ public function __construct( $api, $slug, $container, $file_name ) { $this->api = $api; $this->package_absolute_path = trailingslashit( $container ) . $slug; @@ -17,6 +30,15 @@ if ( ! class_exists( GenericUpdateChecker::class, false ) ) : $this->api->set_slug( $this->slug ); } + /** + * Extracts version information from the package's JSON file. + * + * Parses the JSON file content and looks for version information in the + * packageData section of the JSON structure. + * + * @param string $file Content of the package's JSON file + * @return string The version number found in the file or from update source + */ protected function get_version_from_package_file( $file ) { $file_contents = json_decode( $file, true ); @@ -31,6 +53,15 @@ if ( ! class_exists( GenericUpdateChecker::class, false ) ) : return $this->update_source->version; } + /** + * Returns an empty array since generic packages don't use header names. + * + * This method is implemented to satisfy the abstract parent class requirement + * but returns an empty array as generic packages use JSON format instead of + * header fields. + * + * @return array Empty array as generic packages don't use header fields + */ protected function get_header_names() { return array(); } diff --git a/lib/package-update-checker/PluginUpdateChecker.php b/lib/package-update-checker/PluginUpdateChecker.php index ff561ce..41aca6b 100644 --- a/lib/package-update-checker/PluginUpdateChecker.php +++ b/lib/package-update-checker/PluginUpdateChecker.php @@ -4,10 +4,24 @@ namespace Anyape\PackageUpdateChecker; if ( ! class_exists( PluginUpdateChecker::class, false ) ) : + /** + * Handles WordPress plugin update checking functionality. + * + * This class extends the base UpdateChecker to provide plugin-specific update checking, focusing on plugin header file parsing and metadata retrieval. + */ class PluginUpdateChecker extends UpdateChecker { + /** @var string Path to the main plugin file */ public $package_file = ''; + /** + * Initializes a new instance of the plugin update checker. + * + * @param object $api The Version Control System API instance + * @param string $slug The plugin's slug/directory name + * @param string $container The parent directory containing the plugin + * @param string $file_name The main plugin file name without extension + */ public function __construct( $api, $slug, $container, $file_name ) { $this->api = $api; $this->package_absolute_path = trailingslashit( $container ) . $slug; @@ -19,12 +33,24 @@ if ( ! class_exists( PluginUpdateChecker::class, false ) ) : $this->api->set_slug( $this->slug ); } + /** + * Requests update information for the plugin. + * + * @param string $type The type of package (defaults to 'Plugin') + * @return array|WP_Error Update information array or WP_Error on failure + */ public function request_info( $type = 'Plugin' ) { $info = parent::request_info( $type ); return $info; } + /** + * Extracts version information from the plugin's main file. + * + * @param string $file Content of the plugin's main PHP file + * @return string The version number found in the file or from update source + */ protected function get_version_from_package_file( $file ) { $remote_header = $this->get_file_header( $file ); @@ -33,6 +59,14 @@ if ( ! class_exists( PluginUpdateChecker::class, false ) ) : $remote_header['Version']; } + /** + * Defines the standard WordPress plugin header field names. + * + * These fields correspond to the metadata in the plugin's main PHP file + * that WordPress uses to identify and categorize plugins. + * + * @return array Associative array of plugin header fields and their corresponding names + */ protected function get_header_names() { return array( 'Name' => 'Plugin Name', diff --git a/lib/package-update-checker/ThemeUpdateChecker.php b/lib/package-update-checker/ThemeUpdateChecker.php index 9232a5a..967921e 100644 --- a/lib/package-update-checker/ThemeUpdateChecker.php +++ b/lib/package-update-checker/ThemeUpdateChecker.php @@ -4,10 +4,23 @@ namespace Anyape\PackageUpdateChecker; if ( ! class_exists( ThemeUpdateChecker::class, false ) ) : + /** + * Handles WordPress theme update checking functionality. + * + * This class extends the base UpdateChecker to provide theme-specific update checking, focusing on style.css parsing and theme header information retrieval. + */ class ThemeUpdateChecker extends UpdateChecker { + /** @var string The main theme file to check for updates */ public $package_file = 'style.css'; + /** + * Initializes a new instance of the theme update checker. + * + * @param object $api The Version Control System API instance + * @param string $slug The theme's slug/directory name + * @param string $container The parent directory containing the theme + */ public function __construct( $api, $slug, $container ) { $this->api = $api; $this->package_absolute_path = trailingslashit( $container ) . $slug; @@ -18,12 +31,24 @@ if ( ! class_exists( ThemeUpdateChecker::class, false ) ) : $this->api->set_slug( $this->slug ); } + /** + * Requests update information for the theme. + * + * @param string $type The type of package (defaults to 'Theme') + * @return array|WP_Error Update information array or WP_Error on failure + */ public function request_info( $type = 'Theme' ) { $info = parent::request_info( $type ); return $info; } + /** + * Extracts version information from the theme's style.css file. + * + * @param string $file Content of the theme's style.css file + * @return string The version number found in the file or from update source + */ protected function get_version_from_package_file( $file ) { $remote_header = $this->get_file_header( $file ); @@ -32,6 +57,11 @@ if ( ! class_exists( ThemeUpdateChecker::class, false ) ) : $remote_header['Version']; } + /** + * Defines the standard WordPress theme header field names. + * + * @return array Associative array of theme header fields and their corresponding names + */ protected function get_header_names() { return array( 'Name' => 'Theme Name', diff --git a/lib/package-update-checker/UpdateChecker.php b/lib/package-update-checker/UpdateChecker.php index 2cb482e..1debd82 100644 --- a/lib/package-update-checker/UpdateChecker.php +++ b/lib/package-update-checker/UpdateChecker.php @@ -4,19 +4,47 @@ namespace Anyape\PackageUpdateChecker; if ( ! class_exists( UpdateChecker::class, false ) ) : + /** + * Abstract base class for package update checking functionality. + * + * Provides core functionality for checking updates from various version control systems. + */ abstract class UpdateChecker { + /** @var bool|null Debug mode status */ public $debug_mode = null; + + /** @var string Directory name of the package */ public $directory_name; + + /** @var string Unique identifier for the package */ public $slug; + + /** @var string Absolute path to the package directory */ public $package_absolute_path = ''; + /** @var string Branch name to check for updates, defaults to 'main' */ protected $branch = 'main'; + + /** @var object Version Control System API instance */ protected $api; + + /** @var string Current reference (tag/branch) being checked */ protected $ref; + + /** @var object Source of the update */ protected $update_source; + + /** @var string Path to the main package file */ protected $package_file; + /** + * Triggers an error message when in debug mode. + * + * @param string $message The error message to display + * @param int $error_type The type of error to trigger + * @return void + */ public function trigger_error( $message, $error_type ) { if ( $this->is_debug_mode_enabled() ) { @@ -25,14 +53,16 @@ if ( ! class_exists( UpdateChecker::class, false ) ) : } } + /** + * Extracts header information from a file's content. + * + * @param string $content The content to parse for headers + * @return array Associative array of header fields and their values + */ public function get_file_header( $content ) { $content = (string) $content; - - //WordPress only looks at the first 8 KiB of the file, so we do the same. - $content = substr( $content, 0, 8192 ); - //Normalize line endings. - $content = str_replace( "\r", "\n", $content ); - + $content = substr( $content, 0, 8192 ); // Limit to 8KB + $content = str_replace( "\r", "\n", $content ); // Normalize line endings $headers = $this->get_header_names(); $results = array(); @@ -55,22 +85,45 @@ if ( ! class_exists( UpdateChecker::class, false ) ) : return $results; } + /** + * Sets the branch to check for updates. + * + * @param string $branch The branch name + * @return self + */ public function set_branch( $branch ) { $this->branch = $branch; return $this; } + /** + * Sets authentication credentials for the API. + * + * @param mixed $credentials The authentication credentials + * @return self + */ public function set_authentication( $credentials ) { $this->api->set_authentication( $credentials ); return $this; } + /** + * Gets the VCS API instance. + * + * @return object The VCS API instance + */ public function get_vcs_api() { return $this->api; } + /** + * Requests update information from the repository. + * + * @param string $type The type of package (default: 'Generic') + * @return array|WP_Error Update information or error on failure + */ public function request_info( $type = 'Generic' ) { if ( function_exists( 'set_time_limit' ) ) { @@ -84,7 +137,6 @@ if ( ! class_exists( UpdateChecker::class, false ) ) : if ( $update_source ) { $ref = $update_source->name; } else { - //There's probably a network problem or an authentication error. return new \WP_Error( 'puc-no-update-source', 'Could not retrieve version information from the repository for ' @@ -141,15 +193,25 @@ if ( ! class_exists( UpdateChecker::class, false ) ) : return $info; } + /** + * Extracts version information from the package file. + * + * @param string $file Content of the package file + * @return string Version number + */ abstract protected function get_version_from_package_file( $file ); /** - * @return array Format: ['HeaderKey' => 'Header Name'] + * Gets the header field names to look for in the package file. + * + * @return array Associative array of header keys and their names */ abstract protected function get_header_names(); /** - * @return bool + * Checks if debug mode is enabled. + * + * @return bool True if debug mode is enabled, false otherwise */ protected function is_debug_mode_enabled() { diff --git a/lib/package-update-checker/Vcs/Api.php b/lib/package-update-checker/Vcs/Api.php index 80b61e4..ccb1e3d 100644 --- a/lib/package-update-checker/Vcs/Api.php +++ b/lib/package-update-checker/Vcs/Api.php @@ -4,6 +4,9 @@ namespace Anyape\PackageUpdateChecker\Vcs; if ( ! class_exists( Api::class, false ) ) : + /** + * Abstract class representing a Version Control System (VCS) API. + */ abstract class Api { const STRATEGY_LATEST_RELEASE = 'latest_release'; @@ -82,7 +85,7 @@ if ( ! class_exists( Api::class, false ) ) : } /** - * Figure out which reference ( i.e. tag or branch ) contains the latest version. + * Determine which reference (i.e., tag or branch) contains the latest version. * * @param string $config_branch Start looking in this branch. * @return null|Reference @@ -119,7 +122,7 @@ if ( ! class_exists( Api::class, false ) ) : * "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct * capitalization. * - * Defaults to "readme.txt" ( all lowercase ). + * Defaults to "readme.txt" (all lowercase). * * @return string */ @@ -169,7 +172,7 @@ if ( ! class_exists( Api::class, false ) ) : /** * Get the tag that looks like the highest version number. - * ( Implementations should skip pre-release versions if possible. ) + * (Implementations should skip pre-release versions if possible.) * * @return Reference|null */ @@ -182,15 +185,15 @@ if ( ! class_exists( Api::class, false ) ) : * @return bool */ protected function looks_like_version( $name ) { - //Tag names may be prefixed with "v", e.g. "v1.2.3". + // Tag names may be prefixed with "v", e.g., "v1.2.3". $name = ltrim( $name, 'v' ); - //The version string must start with a number. + // The version string must start with a number. if ( ! is_numeric( substr( $name, 0, 1 ) ) ) { return false; } - //The goal is to accept any SemVer-compatible or "PHP-standardized" version number. + // The goal is to accept any SemVer-compatible or "PHP-standardized" version number. return ( preg_match( '@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name ) === 1 ); } @@ -208,23 +211,23 @@ if ( ! class_exists( Api::class, false ) ) : /** * Sort a list of tags as if they were version numbers. - * Tags that don't look like version number will be removed. + * Tags that don't look like version numbers will be removed. * * @param \stdClass[] $tags Array of tag objects. * @return \stdClass[] Filtered array of tags sorted in descending order. */ protected function sort_tags_by_version( $tags ) { - //Keep only those tags that look like version numbers. + // Keep only those tags that look like version numbers. $version_tags = array_filter( $tags, array( $this, 'is_version_tag' ) ); - //Sort them in descending order. + // Sort them in descending order. usort( $version_tags, array( $this, 'compare_tag_names' ) ); return $version_tags; } /** - * Compare two tags as if they were version number. + * Compare two tags as if they were version numbers. * * @param \stdClass $tag1 Tag object. * @param \stdClass $tag2 Another tag object. @@ -256,7 +259,7 @@ if ( ! class_exists( Api::class, false ) ) : /** * Get the timestamp of the latest commit that changed the specified branch or tag. * - * @param string $ref Reference name ( e.g. branch or tag ). + * @param string $ref Reference name (e.g., branch or tag). * @return string|null */ abstract public function get_latest_commit_time( $ref ); diff --git a/lib/package-update-checker/Vcs/BitbucketApi.php b/lib/package-update-checker/Vcs/BitbucketApi.php index b19946e..e512745 100644 --- a/lib/package-update-checker/Vcs/BitbucketApi.php +++ b/lib/package-update-checker/Vcs/BitbucketApi.php @@ -6,6 +6,10 @@ use WP_Error; if ( ! class_exists( BitbucketApi::class, false ) ) : + /** + * Class BitbucketApi + * Handles interactions with the Bitbucket API. + */ class BitbucketApi extends Api { /** @@ -13,6 +17,13 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : */ protected $app_password; + /** + * BitbucketApi constructor. + * + * @param string $repository_url The URL of the Bitbucket repository. + * @param string|null $app_password Optional. The Bitbucket app password. + * @throws \InvalidArgumentException If the repository URL is invalid. + */ public function __construct( $repository_url, $app_password = null ) { $path = wp_parse_url( $repository_url, PHP_URL_PATH ); @@ -31,7 +42,9 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Check if the VCS is accessible. * - * @return bool|WP_Error + * @param string $url The URL to test. + * @param string|null $app_password Optional. The Bitbucket app password. + * @return bool|WP_Error True if accessible, WP_Error otherwise. */ public static function test( $url, $app_password = null ) { $instance = new self( $url . 'bogus/', $app_password ); @@ -56,6 +69,12 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : ); } + /** + * Get update detection strategies. + * + * @param string $config_branch The branch to check for updates. + * @return array The update detection strategies. + */ protected function get_update_detection_strategies( $config_branch ) { $strategies[ self::STRATEGY_BRANCH ] = function () use ( $config_branch ) { return $this->get_branch( $config_branch ); @@ -68,6 +87,12 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : return $strategies; } + /** + * Get a specific branch. + * + * @param string $branch_name The name of the branch. + * @return Reference|null The branch reference or null if not found. + */ public function get_branch( $branch_name ) { $branch = $this->api( '/refs/branches/' . $branch_name ); @@ -75,9 +100,8 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : return null; } - //The "/src/{stuff}/{path}" endpoint doesn't seem to handle branch names that contain slashes. - //If we don't encode the slash, we get a 404. If we encode it as "%2F", we get a 401. - //To avoid issues, if the branch name is not URL-safe, let's use the commit hash instead. + // The "/src/{something}/{path}" endpoint doesn't handle branch names with slashes. + // If the branch name is not URL-safe, use the commit hash instead. $ref = $branch->name; if ( ( rawurlencode( $ref ) !== $ref ) && isset( $branch->target->hash ) ) { @@ -96,8 +120,8 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Get a specific tag. * - * @param string $tag_name - * @return Reference|null + * @param string $tag_name The name of the tag. + * @return Reference|null The tag reference or null if not found. */ public function get_tag( $tag_name ) { $tag = $this->api( '/refs/tags/' . $tag_name ); @@ -117,9 +141,9 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : } /** - * Get the tag that looks like the highest version number. + * Get the latest tag that looks like the highest version number. * - * @return Reference|null + * @return Reference|null The latest tag reference or null if not found. */ public function get_latest_tag() { $tags = $this->api( '/refs/tags?sort=-target.date' ); @@ -128,10 +152,10 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : return null; } - //Filter and sort the list of tags. + // Filter and sort the list of tags. $version_tags = $this->sort_tags_by_version( $tags->values ); - //Return the first result. + // Return the first result. if ( ! empty( $version_tags ) ) { $tag = $version_tags[0]; @@ -149,8 +173,10 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : } /** - * @param string $ref - * @return string + * Get the download URL for a specific reference. + * + * @param string $ref The reference name (e.g., branch or tag). + * @return string The download URL. */ protected function get_download_url( $ref ) { return sprintf( @@ -164,9 +190,9 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Get the contents of a file from a specific branch or tag. * - * @param string $path File name. - * @param string $ref - * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error. + * @param string $path The file path. + * @param string $ref The reference name (e.g., branch or tag). + * @return null|string The file contents or null if not found. */ public function get_remote_file( $path, $ref = 'main' ) { $response = $this->api( 'src/' . $ref . '/' . ltrim( $path ) ); @@ -181,8 +207,8 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Get the timestamp of the latest commit that changed the specified branch or tag. * - * @param string $ref Reference name ( e.g. branch or tag ). - * @return string|null + * @param string $ref The reference name (e.g., branch or tag). + * @return string|null The timestamp of the latest commit or null if not found. */ public function get_latest_commit_time( $ref ) { $response = $this->api( 'commits/' . $ref ); @@ -197,9 +223,10 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Perform a Bitbucket API 2.0 request. * - * @param string $url - * @param string $version - * @return mixed|WP_Error + * @param string $url The API endpoint URL. + * @param string $version The API version. + * @param bool $override_url Whether to override the base URL. + * @return mixed|WP_Error The API response or WP_Error on failure. */ public function api( $url, $version = '2.0', $override_url = false ) { $url = ltrim( $url, '/' ); @@ -238,8 +265,7 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : if ( 200 === $code ) { if ( $is_src_resource ) { - //Most responses are JSON-encoded, but src resources just - //return raw file contents. + // Most responses are JSON-encoded, but src resources return raw file contents. $document = $body; } else { $document = json_decode( $body ); @@ -266,7 +292,9 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : } /** - * @param array $credentials + * Set authentication credentials. + * + * @param array|string $credentials The authentication credentials. */ public function set_authentication( $credentials ) { parent::set_authentication( $credentials ); @@ -277,7 +305,7 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : /** * Generate the value of the "Authorization" header. * - * @return string + * @return array The authorization headers. */ public function get_authorization_headers() { return array( diff --git a/lib/package-update-checker/Vcs/GitHubApi.php b/lib/package-update-checker/Vcs/GitHubApi.php index 312fead..7c1b4bc 100644 --- a/lib/package-update-checker/Vcs/GitHubApi.php +++ b/lib/package-update-checker/Vcs/GitHubApi.php @@ -8,6 +8,13 @@ use LogicException; if ( ! class_exists( GitHubApi::class, false ) ) : + /** + * Class GitHubApi + * + * This class provides methods to interact with the GitHub API for various operations + * such as fetching releases, tags, branches, and commits. It also handles authentication + * and API request construction. + */ class GitHubApi extends Api { use ReleaseAssetSupport; use ReleaseFilteringFeature; @@ -18,10 +25,17 @@ if ( ! class_exists( GitHubApi::class, false ) ) : protected $access_token; /** - * @var bool + * @var bool Indicates if the download filter has been added. */ private $download_filter_added = false; + /** + * GitHubApi constructor. + * + * @param string $repository_url The URL of the GitHub repository. + * @param string|null $access_token Optional GitHub access token. + * @throws InvalidArgumentException If the repository URL is invalid. + */ public function __construct( $repository_url, $access_token = null ) { $path = wp_parse_url( $repository_url, PHP_URL_PATH ); @@ -40,7 +54,9 @@ if ( ! class_exists( GitHubApi::class, false ) ) : /** * Check if the VCS is accessible. * - * @return bool|WP_Error + * @param string $url The URL to check. + * @param string|null $access_token Optional GitHub access token. + * @return bool|WP_Error True if accessible, false or WP_Error otherwise. */ public static function test( $url, $access_token = null ) { $instance = new self( $url . 'bogus/', $access_token ); @@ -84,20 +100,20 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the latest release from GitHub. + * Retrieve the latest release from GitHub. * - * @return Reference|null + * @return Reference|null The latest release or null if not found. */ public function get_latest_release() { - //The "latest release" endpoint returns one release and always skips pre-releases, so we can only use it if that's compatible with the current filter settings. + // The "latest release" endpoint returns one release and always skips pre-releases, so we can only use it if that's compatible with the current filter settings. if ( $this->should_skip_pre_releases() && ( ( 1 === $this->release_filter_max_releases ) || ! $this->has_custom_release_filter() ) ) { - //Just get the latest release. + // Fetch the latest release. $release = $this->api( '/repos/:user/:repo/releases/latest' ); if ( is_wp_error( $release ) || ! is_object( $release ) || ! isset( $release->tag_name ) ) { @@ -106,7 +122,7 @@ if ( ! class_exists( GitHubApi::class, false ) ) : $found_releases = array( $release ); } else { - //Get a list of the most recent releases. + // Retrieve a list of the most recent releases. $found_releases = $this->api( '/repos/:user/:repo/releases', array( 'per_page' => $this->release_filter_max_releases ) @@ -119,12 +135,12 @@ if ( ! class_exists( GitHubApi::class, false ) ) : foreach ( $found_releases as $release ) { - //Always skip drafts. + // Always skip drafts. if ( isset( $release->draft ) && ! empty( $release->draft ) ) { continue; } - //Skip pre-releases unless specifically included. + // Skip pre-releases unless specifically included. if ( $this->should_skip_pre_releases() && isset( $release->prerelease ) @@ -133,9 +149,9 @@ if ( ! class_exists( GitHubApi::class, false ) ) : continue; } - $version_number = ltrim( $release->tag_name, 'v' ); //Remove the "v" prefix from "v1.2.3". + $version_number = ltrim( $release->tag_name, 'v' ); // Remove the "v" prefix from "v1.2.3". - //Custom release filtering. + // Custom release filtering. if ( ! $this->matches_custom_release_filter( $version_number, $release ) ) { continue; } @@ -156,7 +172,7 @@ if ( ! class_exists( GitHubApi::class, false ) ) : if ( $this->release_assets_enabled ) { - //Use the first release asset that matches the specified regular expression. + // Use the first release asset that matches the specified regular expression. if ( isset( $release->assets, $release->assets[0] ) ) { $matching_assets = array_values( array_filter( $release->assets, array( $this, 'matchesAssetFilter' ) ) ); } else { @@ -173,14 +189,14 @@ if ( ! class_exists( GitHubApi::class, false ) ) : */ $reference->download_url = $matching_assets[0]->url; } else { - //It seems that browser_download_url only works for public repositories. - //Using an access_token doesn't help. Maybe OAuth would work? + // It seems that browser_download_url only works for public repositories. + // Using an access_token doesn't help. Maybe OAuth would work? $reference->download_url = $matching_assets[0]->browser_download_url; } $reference->download_count = $matching_assets[0]->download_count; } elseif ( Api::REQUIRE_RELEASE_ASSETS === $this->release_asset_preference ) { - //None of the assets match the filter, and we're not allowed to fall back to the auto-generated source ZIP. + // None of the assets match the filter, and we're not allowed to fall back to the auto-generated source ZIP. return null; } } @@ -192,9 +208,9 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the tag that looks like the highest version number. + * Retrieve the tag that appears to be the highest version number. * - * @return Reference|null + * @return Reference|null The highest version tag or null if not found. */ public function get_latest_tag() { $tags = $this->api( '/repos/:user/:repo/tags' ); @@ -222,10 +238,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get a branch by name. + * Retrieve a branch by its name. * - * @param string $branch_name - * @return null|Reference + * @param string $branch_name The name of the branch. + * @return null|Reference The branch reference or null if not found. */ public function get_branch( $branch_name ) { $branch = $this->api( '/repos/:user/:repo/branches/' . $branch_name ); @@ -250,11 +266,11 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the latest commit that changed the specified file. + * Retrieve the latest commit that modified the specified file. * - * @param string $filename - * @param string $ref Reference name ( e.g. branch or tag ). - * @return \StdClass|null + * @param string $filename The name of the file. + * @param string $ref Reference name (e.g., branch or tag). + * @return \StdClass|null The latest commit or null if not found. */ public function get_latest_commit( $filename, $ref = 'main' ) { $commits = $this->api( @@ -273,10 +289,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the timestamp of the latest commit that changed the specified branch or tag. + * Retrieve the timestamp of the latest commit that modified the specified branch or tag. * - * @param string $ref Reference name ( e.g. branch or tag ). - * @return string|null + * @param string $ref Reference name (e.g., branch or tag). + * @return string|null The timestamp of the latest commit or null if not found. */ public function get_latest_commit_time( $ref ) { $commits = $this->api( '/repos/:user/:repo/commits', array( 'sha' => $ref ) ); @@ -291,9 +307,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : /** * Perform a GitHub API request. * - * @param string $url - * @param array $query_params - * @return mixed|WP_Error + * @param string $url The API endpoint URL. + * @param array $query_params Optional query parameters. + * @param bool $override_url Whether to override the base URL. + * @return mixed|WP_Error The API response or WP_Error on failure. */ protected function api( $url, $query_params = array(), $override_url = false ) { $base_url = $url; @@ -346,11 +363,11 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Build a fully qualified URL for an API request. + * Construct a fully qualified URL for an API request. * - * @param string $url - * @param array $query_params - * @return string + * @param string $url The API endpoint URL. + * @param array $query_params Optional query parameters. + * @return string The fully qualified URL. */ protected function build_api_url( $url, $query_params ) { $variables = array( @@ -372,11 +389,11 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the contents of a file from a specific branch or tag. + * Retrieve the contents of a file from a specific branch or tag. * - * @param string $path File name. - * @param string $ref - * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error. + * @param string $path The file path. + * @param string $ref The reference name (e.g., branch or tag). + * @return null|string The file contents or null if not found. */ public function get_remote_file( $path, $ref = 'main' ) { $api_url = '/repos/:user/:repo/contents/' . $path; @@ -392,8 +409,8 @@ if ( ! class_exists( GitHubApi::class, false ) ) : /** * Generate a URL to download a ZIP archive of the specified branch/tag/etc. * - * @param string $ref - * @return string + * @param string $ref The reference name (e.g., branch or tag). + * @return string The download URL. */ public function build_archive_download_url( $ref = 'main' ) { $url = sprintf( @@ -407,33 +424,45 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get a specific tag. + * Retrieve a specific tag. * - * @param string $tag_name + * @param string $tag_name The name of the tag. * @return void + * @throws LogicException If the method is not implemented. */ public function get_tag( $tag_name ) { - //The current GitHub update checker doesn't use get_tag, so I didn't bother to implement it. + // The current GitHub update checker doesn't use get_tag, so I didn't bother to implement it. throw new LogicException( 'The ' . __METHOD__ . ' method is not implemented and should not be used.' ); } + /** + * Set the authentication credentials. + * + * @param string|array $credentials The authentication credentials. + */ public function set_authentication( $credentials ) { parent::set_authentication( $credentials ); $this->access_token = is_string( $credentials ) ? $credentials : null; } + /** + * Retrieve the update detection strategies based on the configuration branch. + * + * @param string $config_branch The configuration branch. + * @return array The update detection strategies. + */ protected function get_update_detection_strategies( $config_branch ) { $strategies = array(); if ( 'main' === $config_branch || 'master' === $config_branch ) { - //Use the latest release. + // Use the latest release. $strategies[ self::STRATEGY_LATEST_RELEASE ] = array( $this, 'get_latest_release' ); - //Failing that, use the tag with the highest version number. + // Failing that, use the tag with the highest version number. $strategies[ self::STRATEGY_LATEST_TAG ] = array( $this, 'get_latest_tag' ); } - //Alternatively, just use the branch itself. + // Alternatively, just use the branch itself. $strategies[ self::STRATEGY_BRANCH ] = function () use ( $config_branch ) { return $this->get_branch( $config_branch ); }; @@ -442,9 +471,9 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Get the unchanging part of a release asset URL. Used to identify download attempts. + * Retrieve the unchanging part of a release asset URL. Used to identify download attempts. * - * @return string + * @return string The base URL for release assets. */ protected function get_asset_api_base_url() { return sprintf( @@ -454,6 +483,12 @@ if ( ! class_exists( GitHubApi::class, false ) ) : ); } + /** + * Retrieve the filterable name of a release asset. + * + * @param object $release_asset The release asset object. + * @return string|null The name of the release asset or null if not found. + */ protected function get_filterable_asset_name( $release_asset ) { if ( isset( $release_asset->name ) ) { @@ -464,8 +499,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * @param bool $result - * @return bool + * Add an HTTP request filter. + * + * @param bool $result The result of the filter. + * @return bool The result of the filter. * @internal */ public function add_http_request_filter( $result ) { @@ -482,9 +519,9 @@ if ( ! class_exists( GitHubApi::class, false ) ) : } /** - * Set the HTTP headers that are necessary to download updates from private repositories. + * Set the HTTP headers required to download updates from private repositories. * - * See GitHub docs: + * Refer to GitHub documentation: * * @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset * @link https://developer.github.com/v3/auth/#basic-authentication @@ -496,12 +533,12 @@ if ( ! class_exists( GitHubApi::class, false ) ) : */ public function set_update_download_headers( $request_args, $url = '' ) { - //Is WordPress trying to download one of our release assets? + // Check if WordPress is attempting to download one of our release assets. if ( $this->release_assets_enabled && ( strpos( $url, $this->get_asset_api_base_url() ) !== false ) ) { $request_args['headers']['Accept'] = 'application/octet-stream'; } - //Use Basic authentication, but only if the download is from our repository. + // Use Basic authentication only if the download is from our repository. $repo_api_base_url = $this->build_api_url( '/repos/:user/:repo/', array() ); if ( $this->is_authentication_enabled() && ( strpos( $url, $repo_api_base_url ) ) === 0 ) { @@ -513,8 +550,8 @@ if ( ! class_exists( GitHubApi::class, false ) ) : /** * When following a redirect, the Requests library will automatically forward - * the authorization header to other hosts. We don't want that because it breaks - * AWS downloads and can leak authorization information. + * the authorization header to other hosts. This can cause issues with AWS downloads + * and may expose authorization information. * * @param string $location * @param array $headers @@ -524,17 +561,17 @@ if ( ! class_exists( GitHubApi::class, false ) ) : $repo_api_base_url = $this->build_api_url( '/repos/:user/:repo/', array() ); if ( strpos( $location, $repo_api_base_url ) === 0 ) { - return; //This request is going to GitHub, so it's fine. + return; // This request is going to GitHub, so it's acceptable. } - //Remove the header. + // Remove the authorization header. if ( isset( $headers['Authorization'] ) ) { unset( $headers['Authorization'] ); } } /** - * Generate the value of the "Authorization" header. + * Create the value for the "Authorization" header. * * @return string */ diff --git a/lib/package-update-checker/Vcs/GitLabApi.php b/lib/package-update-checker/Vcs/GitLabApi.php index ac2c35d..50d7413 100644 --- a/lib/package-update-checker/Vcs/GitLabApi.php +++ b/lib/package-update-checker/Vcs/GitLabApi.php @@ -8,25 +8,39 @@ use LogicException; if ( ! class_exists( GitLabApi::class, false ) ) : + /** + * Class GitLabApi + * + * This class interacts with the GitLab API to fetch repository information, + * releases, tags, branches, and other relevant data. + */ class GitLabApi extends Api { use ReleaseAssetSupport; use ReleaseFilteringFeature; /** - * @var string GitLab server host. + * @var string The host of the GitLab server. */ protected $repository_host; /** - * @var string Protocol used by this GitLab server: "http" or "https". + * @var string The protocol used by the GitLab server, either "http" or "https". */ protected $repository_protocol = 'https'; /** - * @var string GitLab authentication token. Optional. + * @var string The GitLab authentication token, which is optional. */ protected $access_token; + /** + * Constructor. + * + * @param string $repository_url The URL of the GitLab repository. + * @param string $access_token The authentication token for GitLab. + * @param string $sub_group The sub-group within the GitLab repository. + * @throws InvalidArgumentException If the repository URL is invalid. + */ public function __construct( $repository_url, $access_token = null, $sub_group = null ) { - //Parse the repository host to support custom hosts. + // Extract the port from the repository URL to support custom hosts. $port = wp_parse_url( $repository_url, PHP_URL_PORT ); if ( ! empty( $port ) ) { @@ -36,17 +50,18 @@ if ( ! class_exists( GitLabApi::class, false ) ) : $this->repository_host = wp_parse_url( $repository_url, PHP_URL_HOST ) . $port; if ( 'gitlab.com' !== $this->repository_host ) { + // Identify the protocol used by the GitLab server. $this->repository_protocol = wp_parse_url( $repository_url, PHP_URL_SCHEME ); } - //Find the repository information + // Extract repository information from the URL. $path = wp_parse_url( $repository_url, PHP_URL_PATH ); if ( preg_match( '@^/?(?P[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches ) ) { $this->user_name = $matches['username']; $this->repository_name = $matches['repository']; } elseif ( ( 'gitlab.com' === $this->repository_host ) ) { - //This is probably a repository in a sub_group, e.g. "/organization/category/repo". + // Handle repositories in sub-groups, e.g., "/organization/category/repo". $parts = explode( '/', trim( $path, '/' ) ); if ( count( $parts ) < 3 ) { @@ -62,16 +77,16 @@ if ( ! class_exists( GitLabApi::class, false ) ) : $this->repository_name = $last_part; } else { - //There could be sub_groups in the URL: gitlab.domain.com/group/sub_group/sub_group2/repository + // Handle URLs with sub-groups: gitlab.domain.com/group/sub_group/sub_group2/repository. if ( null === $sub_group ) { $path = str_replace( trailingslashit( $sub_group ), '', $path ); } - //This is not a traditional url, it could be gitlab is in a deeper subdirectory. - //Get the path segments. + // Handle non-traditional URLs where GitLab is in a deeper subdirectory. + // Extract the path segments. $segments = explode( '/', untrailingslashit( ltrim( $path, '/' ) ) ); - //We need at least /user-name/repository-name/ + // Ensure there are at least /user-name/repository-name/ segments. if ( count( $segments ) < 2 ) { throw new InvalidArgumentException( esc_html( @@ -80,17 +95,17 @@ if ( ! class_exists( GitLabApi::class, false ) ) : ); } - //Get the username and repository name. + // Extract the username and repository name. $username_repo = array_splice( $segments, -2, 2 ); $this->user_name = $username_repo[0]; $this->repository_name = $username_repo[1]; - //Append the remaining segments to the host if there are segments left. + // Append remaining segments to the host if any segments are left. if ( count( $segments ) > 0 ) { $this->repository_host = trailingslashit( $this->repository_host ) . implode( '/', $segments ); } - //Add sub_groups to username. + // Add sub-groups to the username if provided. if ( null !== $sub_group ) { $this->user_name = $username_repo[0] . '/' . untrailingslashit( $sub_group ); } @@ -102,7 +117,9 @@ if ( ! class_exists( GitLabApi::class, false ) ) : /** * Check if the VCS is accessible. * - * @return bool|WP_Error + * @param string $url The URL to check. + * @param string $access_token The authentication token for GitLab. + * @return bool|WP_Error True if accessible, WP_Error otherwise. */ public static function test( $url, $access_token = null ) { $instance = new self( $url . 'bogus/', $access_token ); @@ -122,9 +139,9 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get the latest release from GitLab. + * Retrieve the latest release from GitLab. * - * @return Reference|null + * @return Reference|null The latest release or null if not found. */ public function get_latest_release() { $releases = $this->api( '/:id/releases', array( 'per_page' => $this->release_filter_max_releases ) ); @@ -136,10 +153,10 @@ if ( ! class_exists( GitLabApi::class, false ) ) : foreach ( $releases as $release ) { if ( - //Skip invalid/unsupported releases. + // Skip invalid or unsupported releases. ! is_object( $release ) || ! isset( $release->tag_name ) - //Skip upcoming releases. + // Skip upcoming releases. || ( ! empty( $release->upcoming_release ) && $this->should_skip_pre_releases() @@ -148,9 +165,9 @@ if ( ! class_exists( GitLabApi::class, false ) ) : continue; } - $version_number = ltrim( $release->tag_name, 'v' ); //Remove the "v" prefix from "v1.2.3". + $version_number = ltrim( $release->tag_name, 'v' ); // Remove the "v" prefix from "v1.2.3". - //Apply custom filters. + // Apply custom filters. if ( ! $this->matches_custom_release_filter( $version_number, $release ) ) { continue; } @@ -158,7 +175,7 @@ if ( ! class_exists( GitLabApi::class, false ) ) : $download_url = $this->find_release_download_url( $release ); if ( empty( $download_url ) ) { - //The latest release doesn't have valid download URL. + // The latest release doesn't have a valid download URL. return null; } @@ -177,8 +194,10 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * @param object $release - * @return string|null + * Locate the download URL for a release asset. + * + * @param object $release The release object. + * @return string|null The download URL or null if not found. */ protected function find_release_download_url( $release ) { @@ -186,7 +205,7 @@ if ( ! class_exists( GitLabApi::class, false ) ) : if ( isset( $release->assets, $release->assets->links ) ) { - //Use the first asset link where the URL matches the filter. + // Use the first asset link that matches the filter. foreach ( $release->assets->links as $link ) { if ( $this->matches_asset_filter( $link ) ) { @@ -196,12 +215,12 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } if ( Api::REQUIRE_RELEASE_ASSETS === $this->release_asset_preference ) { - //Falling back to source archives is not allowed, so give up. + // Do not fall back to source archives, so return null. return null; } } - //Use the first source code archive that's in ZIP format. + // Use the first source code archive in ZIP format. foreach ( $release->assets->sources as $source ) { if ( isset( $source->format ) && ( 'zip' === $source->format ) ) { @@ -213,9 +232,9 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get the tag that looks like the highest version number. + * Retrieve the tag that appears to be the highest version number. * - * @return Reference|null + * @return Reference|null The latest tag or null if not found. */ public function get_latest_tag() { $tags = $this->api( '/:id/repository/tags' ); @@ -243,10 +262,10 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get a branch by name. + * Retrieve a branch by its name. * - * @param string $branch_name - * @return null|Reference + * @param string $branch_name The name of the branch. + * @return null|Reference The branch reference or null if not found. */ public function get_branch( $branch_name ) { $branch = $this->api( '/:id/repository/branches/' . $branch_name ); @@ -271,10 +290,10 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get the timestamp of the latest commit that changed the specified branch or tag. + * Retrieve the timestamp of the latest commit that modified the specified branch or tag. * - * @param string $ref Reference name ( e.g. branch or tag ). - * @return string|null + * @param string $ref The reference name (e.g., branch or tag). + * @return string|null The timestamp of the latest commit or null if not found. */ public function get_latest_commit_time( $ref ) { $commits = $this->api( '/:id/repository/commits/', array( 'ref_name' => $ref ) ); @@ -287,11 +306,11 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Perform a GitLab API request. + * Execute a GitLab API request. * - * @param string $url - * @param array $query_params - * @return mixed|WP_Error + * @param string $url The API endpoint URL. + * @param array $query_params The query parameters for the request. + * @return mixed|WP_Error The API response or WP_Error on failure. */ protected function api( $url, $query_params = array(), $override_url = false ) { @@ -338,11 +357,11 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Build a fully qualified URL for an API request. + * Construct a fully qualified URL for an API request. * - * @param string $url - * @param array $query_params - * @return string + * @param string $url The API endpoint URL. + * @param array $query_params The query parameters for the request. + * @return string The fully qualified URL. */ protected function build_api_url( $url, $query_params ) { $variables = array( @@ -366,11 +385,11 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get the contents of a file from a specific branch or tag. + * Retrieve the contents of a file from a specific branch or tag. * - * @param string $path File name. - * @param string $ref - * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error. + * @param string $path The file name. + * @param string $ref The reference name (e.g., branch or tag). + * @return null|string The file contents or null if the file doesn't exist or there's an error. */ public function get_remote_file( $path, $ref = 'main' ) { $response = $this->api( '/:id/repository/files/' . $path, array( 'ref' => $ref ) ); @@ -385,8 +404,8 @@ if ( ! class_exists( GitLabApi::class, false ) ) : /** * Generate a URL to download a ZIP archive of the specified branch/tag/etc. * - * @param string $ref - * @return string + * @param string $ref The reference name (e.g., branch or tag). + * @return string The download URL. */ public function build_archive_download_url( $ref = 'main' ) { $url = sprintf( @@ -401,15 +420,21 @@ if ( ! class_exists( GitLabApi::class, false ) ) : } /** - * Get a specific tag. + * Retrieve a specific tag. * - * @param string $tag_name + * @param string $tag_name The name of the tag. * @return void */ public function get_tag( $tag_name ) { throw new LogicException( 'The ' . __METHOD__ . ' method is not implemented and should not be used.' ); } + /** + * Get the strategies for detecting updates. + * + * @param string $config_branch The configuration branch. + * @return array The update detection strategies. + */ protected function get_update_detection_strategies( $config_branch ) { $strategies = array(); @@ -425,12 +450,24 @@ if ( ! class_exists( GitLabApi::class, false ) ) : return $strategies; } + /** + * Set the authentication credentials. + * + * @param string $credentials The authentication credentials. + * @return void + */ public function set_authentication( $credentials ) { parent::set_authentication( $credentials ); $this->access_token = is_string( $credentials ) ? $credentials : null; } + /** + * Retrieve the filterable asset name. + * + * @param object $release_asset The release asset object. + * @return string|null The asset name or null if not found. + */ protected function get_filterable_asset_name( $release_asset ) { if ( isset( $release_asset->url ) ) { @@ -443,7 +480,7 @@ if ( ! class_exists( GitLabApi::class, false ) ) : /** * Generate the value of the "Authorization" header. * - * @return string + * @return string The authorization header value. */ public function get_authorization_headers() { return array( 'PRIVATE-TOKEN' => $this->access_token ); diff --git a/lib/package-update-checker/Vcs/Reference.php b/lib/package-update-checker/Vcs/Reference.php index 12c801f..4e43c12 100644 --- a/lib/package-update-checker/Vcs/Reference.php +++ b/lib/package-update-checker/Vcs/Reference.php @@ -5,43 +5,55 @@ namespace Anyape\PackageUpdateChecker\Vcs; if ( ! class_exists( Reference::class, false ) ) : /** - * This class represents a VCS branch or tag. It's intended as a read only, short-lived container - * that only exists to provide a limited degree of type checking. + * Class Reference + * + * This class represents a VCS branch or tag. It serves as a read-only, temporary container + * that provides a limited degree of type checking. * * @property string $name - * @property string|null version + * @property string|null $version * @property string $download_url * @property string $updated - * * @property int|null $downloadCount */ class Reference { private $properties = array(); + /** + * Constructor. + * + * @param array $properties The properties to initialize the reference with. + */ public function __construct( $properties = array() ) { $this->properties = $properties; } /** - * @param string $name - * @return mixed|null + * Magic getter method. + * + * @param string $name The property name. + * @return mixed|null The property value or null if it doesn't exist. */ public function __get( $name ) { return array_key_exists( $name, $this->properties ) ? $this->properties[ $name ] : null; } /** - * @param string $name - * @param mixed $value + * Magic setter method. + * + * @param string $name The property name. + * @param mixed $value The value to set. */ public function __set( $name, $value ) { $this->properties[ $name ] = $value; } /** - * @param string $name - * @return bool + * Magic isset method. + * + * @param string $name The property name. + * @return bool True if the property is set, false otherwise. */ public function __isset( $name ) { return isset( $this->properties[ $name ] ); diff --git a/lib/package-update-checker/Vcs/ReleaseAssetSupport.php b/lib/package-update-checker/Vcs/ReleaseAssetSupport.php index 6cc3cbf..5314e99 100644 --- a/lib/package-update-checker/Vcs/ReleaseAssetSupport.php +++ b/lib/package-update-checker/Vcs/ReleaseAssetSupport.php @@ -4,21 +4,33 @@ namespace Anyape\PackageUpdateChecker\Vcs; if ( ! trait_exists( ReleaseAssetSupport::class, false ) ) : + /** + * Trait ReleaseAssetSupport + * + * Provides functionality for handling release assets in version control systems. + * Implements methods for enabling, disabling, and filtering release assets + * during the update process. + */ trait ReleaseAssetSupport { /** - * @var bool Whether to download release assets instead of the auto-generated - * source code archives. + * Whether to download release assets instead of the auto-generated source code archives. + * + * @var bool */ protected $release_assets_enabled = false; /** - * @var string|null Regular expression that's used to filter release assets - * by file name or URL. Optional. + * Regular expression that's used to filter release assets by file name or URL. + * + * @var string|null Optional regular expression for asset filtering. */ protected $asset_filter_regex = null; /** * How to handle releases that don't have any matching release assets. * + * Controls the behavior when no matching assets are found in a release. + * Uses Api::PREFER_RELEASE_ASSETS constant as default. + * * @var int */ protected $release_asset_preference = Api::PREFER_RELEASE_ASSETS; @@ -29,10 +41,11 @@ if ( ! trait_exists( ReleaseAssetSupport::class, false ) ) : * If the latest release contains no usable assets, the update checker * will fall back to using the automatically generated ZIP archive. * - * @param string|null $nameRegex Optional. Use only those assets where + * @param string|null $name_regex Optional. Use only those assets where * the file name or URL matches this regex. - * @param int $preference Optional. How to handle releases that don't have - * any matching release assets. + * @param int $preference Optional. How to handle releases that don't have + * any matching release assets. + * @return void */ public function enable_release_assets( $name_regex = null, $preference = Api::PREFER_RELEASE_ASSETS ) { $this->release_assets_enabled = true; @@ -43,6 +56,8 @@ if ( ! trait_exists( ReleaseAssetSupport::class, false ) ) : /** * Disable release assets. * + * Disables the use of release assets and clears any existing asset filters. + * * @return void * @noinspection PhpUnused -- Public API */ @@ -54,8 +69,11 @@ if ( ! trait_exists( ReleaseAssetSupport::class, false ) ) : /** * Does the specified asset match the name regex? * - * @param mixed $releaseAsset Data type and structure depend on the host/API. - * @return bool + * Checks if a given release asset matches the configured name regex filter. + * If no filter is set, accepts all assets by default. + * + * @param mixed $release_asset Data type and structure depend on the host/API. + * @return bool True if the asset matches the filter or if no filter is set. */ protected function matches_asset_filter( $release_asset ) { @@ -76,8 +94,8 @@ if ( ! trait_exists( ReleaseAssetSupport::class, false ) ) : /** * Get the part of asset data that will be checked against the filter regex. * - * @param mixed $releaseAsset - * @return string|null + * @param mixed $release_asset The release asset object to extract the name from. + * @return string|null The filterable name of the asset or null if not available. */ abstract protected function get_filterable_asset_name( $release_asset ); } diff --git a/lib/package-update-checker/Vcs/ReleaseFilteringFeature.php b/lib/package-update-checker/Vcs/ReleaseFilteringFeature.php index 5130d04..c379883 100644 --- a/lib/package-update-checker/Vcs/ReleaseFilteringFeature.php +++ b/lib/package-update-checker/Vcs/ReleaseFilteringFeature.php @@ -4,17 +4,30 @@ namespace Anyape\PackageUpdateChecker\Vcs; if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : + /** + * Trait ReleaseFilteringFeature + * + * Provides functionality for filtering VCS releases based on version numbers, + * custom callbacks, and release types. Allows for flexible release selection + * through customizable filtering mechanisms. + */ trait ReleaseFilteringFeature { /** + * Callback function for custom release filtering. + * * @var callable|null */ protected $release_filter_callback = null; /** + * Maximum number of releases to check during filtering. + * * @var int */ protected $release_filter_max_releases = 1; /** + * Release filtering type setting. + * * @var string One of the Api::RELEASE_FILTER_* constants. */ protected $release_filter_by_type = Api::RELEASE_FILTER_SKIP_PRERELEASE; @@ -24,12 +37,13 @@ if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : * * Setting a new filter will override the old filter, if any. * - * @param callable $callback A callback that accepts a version number and a release - * object, and returns a boolean. - * @param int $releaseTypes One of the Api::RELEASE_FILTER_* constants. - * @param int $maxReleases Optional. The maximum number of recent releases to examine - * when trying to find a release that matches the filter. 1 to 100. + * @param callable $callback A callback that accepts a version number and a release + * object, and returns a boolean. + * @param int $release_types One of the Api::RELEASE_FILTER_* constants. + * @param int $max_releases Optional. The maximum number of recent releases to examine + * when trying to find a release that matches the filter. 1 to 100. * @return $this + * @throws \InvalidArgumentException When max_releases is not between 1 and 100. */ public function set_release_filter( $callback, @@ -61,11 +75,11 @@ if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : } /** - * Filter releases by their version number. + * Filter releases by their version number using a regular expression. * - * @param string $regex A regular expression. The release version number must match this regex. - * @param int $releaseTypes - * @param int $maxReleasesToExamine + * @param string $regex A regular expression pattern to match version numbers. + * @param int $release_types Type of releases to filter (Api::RELEASE_FILTER_*). + * @param int $max_releases_to_examine Maximum number of releases to check. * @return $this * @noinspection PhpUnused -- Public API */ @@ -84,9 +98,11 @@ if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : } /** - * @param string $versionNumber The detected release version number. - * @param object $releaseObject Varies depending on the host/API. - * @return bool + * Checks if a specific version number and release object match the custom filter criteria. + * + * @param string $version_number The detected release version number. + * @param object $release_object Release information object from the API. + * @return bool True if the release matches the filter criteria, false otherwise. */ protected function matches_custom_release_filter( $version_number, $release_object ) { @@ -98,7 +114,9 @@ if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : } /** - * @return bool + * Determines if pre-release versions should be excluded from updates. + * + * @return bool True if pre-releases should be skipped, false otherwise. */ protected function should_skip_pre_releases() { //Maybe this could be a bitfield in the future, if we need to support more release types. @@ -106,7 +124,9 @@ if ( ! trait_exists( ReleaseFilteringFeature::class, false ) ) : } /** - * @return bool + * Checks if a custom release filter has been set and is callable. + * + * @return bool True if a custom filter is set and callable, false otherwise. */ protected function has_custom_release_filter() { return isset( $this->release_filter_callback ) && is_callable( $this->release_filter_callback ); diff --git a/lib/package-update-checker/Vcs/VcsCheckerMethods.php b/lib/package-update-checker/Vcs/VcsCheckerMethods.php index 1e7832b..78b1a45 100644 --- a/lib/package-update-checker/Vcs/VcsCheckerMethods.php +++ b/lib/package-update-checker/Vcs/VcsCheckerMethods.php @@ -4,6 +4,12 @@ namespace Anyape\PackageUpdateChecker\Vcs; if ( ! trait_exists( VcsCheckerMethods::class, false ) ) : + /** + * Trait VcsCheckerMethods + * + * Provides common functionality for version control system (VCS) update checking. + * Implements methods for branch management, authentication, and configuration display. + */ trait VcsCheckerMethods { /** @@ -15,6 +21,12 @@ if ( ! trait_exists( VcsCheckerMethods::class, false ) ) : */ protected $api = null; + /** + * Sets the branch to check for updates. + * + * @param string $branch The branch name to set. + * @return $this For method chaining. + */ public function set_branch( $branch ) { $this->branch = $branch; @@ -24,8 +36,8 @@ if ( ! trait_exists( VcsCheckerMethods::class, false ) ) : /** * Set authentication credentials. * - * @param array|string $credentials - * @return $this + * @param array|string $credentials Authentication credentials for the VCS API. + * @return $this For method chaining. */ public function set_authentication( $credentials ) { $this->api->set_authentication( $credentials ); @@ -34,12 +46,20 @@ if ( ! trait_exists( VcsCheckerMethods::class, false ) ) : } /** - * @return Api + * Gets the VCS API client instance. + * + * @return Api The VCS API client instance. */ public function get_vcs_api() { return $this->api; } + /** + * Displays the configuration information in the given panel. + * + * @param object $panel The panel object used to display configuration. + * @return void + */ public function on_display_configuration( $panel ) { parent::on_display_configuration( $panel ); diff --git a/lib/wp-update-migrate/class-wp-update-migrate.php b/lib/wp-update-migrate/class-wp-update-migrate.php index 694c92d..018b0ae 100644 --- a/lib/wp-update-migrate/class-wp-update-migrate.php +++ b/lib/wp-update-migrate/class-wp-update-migrate.php @@ -3,8 +3,9 @@ * WP Update Migrate * WordPress plugins and themes update path library. * - * @author Alexandre Froger - * @version 1.5 + * @package UpdatePulseServer + * @subpackage WP_Update_Migrate + * @version 1.5.0 */ /*================================================================================================ */ @@ -73,23 +74,81 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! class_exists( 'WP_Update_Migrate' ) ) { + /** + * WP_Update_Migrate class + * + * Handles the migration and update process for WordPress plugins and themes. + */ class WP_Update_Migrate { + /** + * The current version of the WP_Update_Migrate class. + * + * @var string + */ const VERSION = '1.5.0'; + /** + * @var string Information about the failed update. + */ protected $failed_update_info; + + /** + * @var string Information about the successful update. + */ protected $success_update_info; + + /** + * @var string The name of the package. + */ protected $package_name; + + /** + * @var string The prefix of the package. + */ protected $package_prefix; + + /** + * @var string The directory of the package. + */ protected $package_dir; + + /** + * @var string The version to update to. + */ protected $to_version; + + /** + * @var string The version to update from. + */ protected $from_version; + + /** + * @var bool The result of the update. + */ protected $update_result; + + /** + * @var string The type of the package (plugin or theme). + */ protected $package_type; + + /** + * @var string The handle of the package. + */ protected $package_handle; + /** + * @var WP_Update_Migrate The instance of the class. + */ protected static $instance; + /** + * Constructor + * + * @param string $package_handle The handle of the package. + * @param string $package_prefix The prefix of the package. + */ private function __construct( $package_handle, $package_prefix ) { if ( ! wp_doing_ajax() ) { @@ -105,6 +164,13 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { wp_cache_set( $package_prefix, $this, 'wp-update-migrate' ); } + /** + * Get instance + * + * @param string $package_handle The handle of the package. + * @param string $package_prefix The prefix of the package. + * @return WP_Update_Migrate The instance of the class. + */ public static function get_instance( $package_handle, $package_prefix ) { wp_cache_add_non_persistent_groups( 'wp-update-migrate' ); @@ -117,10 +183,18 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return self::$instance; } + /** + * Get result + * + * @return bool The result of the update. + */ public function get_result() { return $this->update_result; } + /** + * Initialize the package + */ public function init() { if ( 'plugin' === $this->package_type ) { @@ -161,24 +235,35 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { } } + /** + * Display update failed notice + */ public function update_failed_notice() { $class = 'notice notice-error is-dismissible'; $message = '

' . $this->failed_update_info . '

'; - // translators: %1$s is the package type + // Translators: %1$s is the package type $message .= '

' . sprintf( esc_html__( 'The %1$s may not have any effect until the issues are resolved.', 'wp-update-migrate' ), $this->package_type ) . '

'; echo wp_kses_post( sprintf( '
%2$s
', $class, $message ) ); } + /** + * Display update success notice + */ public function update_success_notice() { $class = 'notice notice-success is-dismissible'; - // translators: %1$s is the package version to update to + // Translators: %1$s is the package version to update to $title = $this->package_name . ' - ' . sprintf( __( 'Success updating to version %1$s', 'wp-update-migrate' ), $this->to_version ); $message = '

' . $title . '

' . $this->success_update_info . '

'; echo wp_kses_post( sprintf( '
%2$s
', $class, $message ) ); } + /** + * Get content directory + * + * @return string The content directory path. + */ protected static function get_content_dir() { WP_Filesystem(); @@ -191,6 +276,11 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return $wp_filesystem->wp_content_dir(); } + /** + * Build update path + * + * @return array The update path. + */ protected function build_update_path() { $file_list = glob( $this->package_dir . 'updates' . DIRECTORY_SEPARATOR . '*.php' ); $update_path = array(); @@ -207,6 +297,11 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return $update_path; } + /** + * Update the package + * + * @return mixed The result of the update. + */ protected function update() { $update_path = $this->build_update_path(); $result = true; @@ -259,6 +354,12 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return $result; } + /** + * Perform the update for a specific version + * + * @param string $version The version to update to. + * @return mixed The result of the update. + */ protected function do_update( $version ) { $error = false; $result = false; @@ -278,7 +379,7 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { $error = new WP_Error( __METHOD__, sprintf( - // translators: %1$s is the missing function name, %2$s is the package type, %3$s is the path to WP_CONTENT_DIR + // Translators: %1$s is the missing function name, %2$s is the package type, %3$s is the path to WP_CONTENT_DIR __( '
The update failed: function %1$s not found.
Please restore the previously used version of the %2$s, or delete the %2$s and its files in the %2$s directory if any and install the latest version.', 'wp-update-migrate' ), $function_name, $this->package_type, @@ -295,7 +396,7 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { $result = $this->update_package_version( $version ); if ( true === $result ) { - // translators: %1$s is the version we just updated to + // Translators: %1$s is the version we just updated to $this->handle_success( sprintf( __( 'Updates for version %1$s applied.', 'wp-update-migrate' ), $version ) ); return true; @@ -305,6 +406,11 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { } } + /** + * Initialize the package type + * + * @param string $package_prefix The prefix of the package. + */ protected function init_package_type( $package_prefix ) { $hook = current_filter(); @@ -317,14 +423,32 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { } } + /** + * Check if update file exists for a specific version + * + * @param string $version The version to check. + * @return bool True if the update file exists, false otherwise. + */ protected function update_file_exists_for_version( $version ) { return file_exists( $this->package_dir . 'updates' . DIRECTORY_SEPARATOR . $version . '.php' ); } + /** + * Get the update file path for a specific version + * + * @param string $version The version to get the file path for. + * @return string The update file path. + */ protected function get_update_file_path_for_version( $version ) { return $this->package_dir . 'updates' . DIRECTORY_SEPARATOR . $version . '.php'; } + /** + * Update the package version + * + * @param string $version The version to update to. + * @return mixed The result of the update. + */ protected function update_package_version( $version ) { if ( ! version_compare( $this->from_version, $version, '=' ) ) { @@ -336,7 +460,7 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { if ( ! $result ) { $result = new WP_Error( __METHOD__, - // translators: %1$s is the package prefix, %2$s is the package type, %3$s is the version number we're trying to update to + // Translators: %1$s is the package prefix, %2$s is the package type, %3$s is the version number we're trying to update to sprintf( __( 'Failed to update the %1$s_%2$s_version to %3$s in the options table.', 'wp-update-migrate' ), $this->package_prefix, $this->package_type, $version ) ); } @@ -344,16 +468,22 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return $result; } + /** + * Handle error + * + * @param WP_Error|null $error The error object. + * @return bool False indicating an error occurred. + */ protected function handle_error( $error = null ) { $error_title = $this->package_name . ' - ' . sprintf( - // translators: %1$s is the package version to update to + // Translators: %1$s is the package version to update to __( 'Error updating to version %1$s', 'wp-update-migrate' ), $this->to_version ); $error_message = sprintf( - // translators: %1$s is the path to WP_CONTENT_DIR, %2$s is the package type + // Translators: %1$s is the path to WP_CONTENT_DIR, %2$s is the package type __( 'An unexpected error has occured during the update.
Please restore the previously used version of the %2$s, or delete the %2$s and its files in the %1$s directory if any and install the latest version.', 'wp-update-migrate' ), self::get_content_dir(), $this->package_type @@ -368,6 +498,11 @@ if ( ! class_exists( 'WP_Update_Migrate' ) ) { return false; } + /** + * Handle success + * + * @param string|null $message The success message. + */ protected function handle_success( $message = null ) { $this->success_update_info .= $message . '
'; } diff --git a/optimisation/upserv-default-optimizer.php b/optimisation/upserv-default-optimizer.php index f0ba859..84984ad 100644 --- a/optimisation/upserv-default-optimizer.php +++ b/optimisation/upserv-default-optimizer.php @@ -30,27 +30,44 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } +/** + * Main optimization function for UpdatePulse Server API requests. + * + * Determines if the current request is an UpdatePulse API call and applies + * performance optimizations by removing unnecessary WordPress hooks. + * + * @since 2.0 + * + * @return void + */ function upserv_muplugins_loaded() { + // Get host information from server variables $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : false; $url = ''; + // Fallback to SERVER_NAME if HTTP_HOST is not available if ( ! $host && isset( $_SERVER['SERVER_NAME'] ) ) { $host = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); } + // Construct the full URL if host and request URI are available if ( $host && isset( $_SERVER['REQUEST_URI'] ) ) { $request_uri = sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $url = sanitize_url( 'https://' . $host . $request_uri ); } + // Parse URL to determine if this is an API request $path = str_replace( trailingslashit( home_url() ), '', $url ); $frags = explode( '/', $path ); $doing_api = preg_match( '/^updatepulse-server-((.*?)-api|nonce|token)$/', $frags[0] ); $hooks = array(); + // Apply optimizations if this is an API request if ( apply_filters( 'upserv_mu_optimizer_doing_api_request', $doing_api ) ) { + // Notify before applying optimizations do_action( 'upserv_mu_optimizer_default_pre_apply' ); + // Define hooks to be removed $hooks = apply_filters( 'upserv_mu_optimizer_remove_all_hooks', array( @@ -86,18 +103,24 @@ function upserv_muplugins_loaded() { ) ); + // Remove all filters from specified hooks foreach ( $hooks as $hook ) { remove_all_filters( $hook ); } + // Override theme directories to prevent theme loading add_filter( 'template_directory', fn() => __DIR__, PHP_INT_MAX - 100, 0 ); add_filter( 'stylesheet_directory', fn() => __DIR__, PHP_INT_MAX - 100, 0 ); + // Disable advanced cache add_filter( 'enable_loading_advanced_cache_dropin', fn() => false, PHP_INT_MAX - 100, 0 ); + // Notify after applying optimizations do_action( 'upserv_mu_optimizer_default_applied' ); } + // Set up caching for UpdatePulse Server wp_cache_add_non_persistent_groups( 'updatepulse-server' ); wp_cache_set( 'upserv_mu_doing_api', $doing_api, 'updatepulse-server' ); + // Signal that the optimizer is ready do_action( 'upserv_mu_optimizer_ready', $doing_api, @@ -107,6 +130,10 @@ function upserv_muplugins_loaded() { ); } +/** + * Hook the optimization function to run on muplugins_loaded, + * except when running in WP-CLI environment. + */ if ( ! defined( 'WP_CLI' ) ) { add_action( 'muplugins_loaded', 'upserv_muplugins_loaded', 0 ); } diff --git a/tests.php b/tests.php index bf2a309..5fbbfc7 100644 --- a/tests.php +++ b/tests.php @@ -1,13 +1,40 @@ queries ) - count( $upserv_queries_before ) ); - $scripts_stats = 'Number of included/required scripts by the plugin: ' . count( $scripts ); - $mem_stats = 'Server memory used to run the plugin: ' . upserv_get_formatted_memory( $mem_after - $upserv_mem_before ) . ' / ' . ini_get( 'memory_limit' ); - $elapsed_time = 'N/A'; + $query_list = array(); + // Calculate diff stats for plugin-specific usage + $plugin_queries = count( $wpdb->queries ) - count( $upserv_queries_before ); + + // Format stats for logging + $query_stats = 'Number of queries executed by the plugin: ' . $plugin_queries; + $scripts_stats = 'Number of included/required scripts by the plugin: ' . count( $scripts ); + $mem_stats = 'Server memory used to run the plugin: ' . + upserv_get_formatted_memory( $mem_after - $upserv_mem_before ) . + ' / ' . ini_get( 'memory_limit' ); + + // Format query data foreach ( $wpdb->queries as $query ) { $query_list[] = reset( $query ); } + // Calculate execution time if available + $elapsed_time = 'N/A'; + if ( ! empty( $_SERVER['REQUEST_TIME_FLOAT'] ) ) { $req_time_float = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_TIME_FLOAT'] ) ); @@ -79,16 +141,21 @@ function upserv_performance_stats_log() { } } + // Log all collected metrics upserv_tests_log( '========================================================' ); upserv_tests_log( '--- Start load tests ---' ); upserv_tests_log( 'Time elapsed: ' . $elapsed_time . 'sec' ); - upserv_tests_log( 'Total server memory used: ' . upserv_get_formatted_memory( $mem_after ) . ' / ' . ini_get( 'memory_limit' ) ); + upserv_tests_log( + 'Total server memory used: ' . upserv_get_formatted_memory( $mem_after ) . + ' / ' . ini_get( 'memory_limit' ) + ); upserv_tests_log( 'Total number of queries: ' . count( $wpdb->queries ) ); upserv_tests_log( 'Total number of scripts: ' . count( $scripts_after ) ); upserv_tests_log( $mem_stats ); upserv_tests_log( $query_stats ); upserv_tests_log( $scripts_stats ); + // Log detailed information based on configuration if ( $upserv_tests_show_queries_details ) { upserv_tests_log( 'Queries: ', $query_list ); } @@ -99,4 +166,6 @@ function upserv_performance_stats_log() { upserv_tests_log( '--- End load tests ---' ); } + +// Register the performance logging function to run at the end of request processing add_action( 'shutdown', 'upserv_performance_stats_log' ); diff --git a/uninstall.php b/uninstall.php index eba5e18..3ab53ec 100644 --- a/uninstall.php +++ b/uninstall.php @@ -1,16 +1,42 @@ dirlist( $mu_plugin_path ); @@ -37,19 +64,25 @@ function upserv_uninstall() { } } + // Remove the plugin's uploads directory $wp_upload_dir = wp_upload_dir(); $upserv_dir = trailingslashit( $wp_upload_dir['basedir'] . '/updatepulse-server' ); $wp_filesystem->delete( $upserv_dir, true ); + // Clean up Action Scheduler entries if available if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'upserv_cleanup' ); } + // Remove default WordPress cron hooks wp_unschedule_hook( 'upserv_cleanup' ); + + // Delete all plugin options and database tables $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE `option_name` LIKE %s", '%upserv_%' ) ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}upserv_licenses;" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}upserv_nonce;" ); } +// Execute the uninstall process upserv_uninstall(); diff --git a/updatepulse-server.php b/updatepulse-server.php index a0dd77f..3a7ff6f 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -11,12 +11,19 @@ * Domain Path: /languages * * @package UPServ + * + * UpdatePulse Server is a WordPress plugin that enables users to host their own + * update server for WordPress themes and plugins, or generic packages. + * It handles license verification, package distribution, and update management + * through a set of APIs and managers. */ +// Exit if accessed directly to prevent unauthorized access if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } +// Skip processing during WordPress heartbeat requests for performance optimization if ( defined( 'DOING_AJAX' ) && DOING_AJAX && @@ -25,6 +32,7 @@ if ( return; } +// Store performance metrics when debugging is enabled if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { global $wpdb, $upserv_mem_before, $upserv_scripts_before, $upserv_queries_before; @@ -33,6 +41,7 @@ if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SAVEQUERIES' ) && SAVEQUERIE $upserv_queries_before = $wpdb->queries; } +// Import required namespace components for the plugin use Anyape\UpdatePulse\Server\Nonce\Nonce; use Anyape\UpdatePulse\Server\API\License_API; use Anyape\UpdatePulse\Server\API\Webhook_API; @@ -47,6 +56,7 @@ use Anyape\UpdatePulse\Server\Manager\API_Manager; use Anyape\UpdatePulse\Server\Scheduler\Scheduler; use Anyape\UpdatePulse\Server\UPServ; +// Define essential plugin constants for file paths and URLs if ( ! defined( 'UPSERV_PLUGIN_PATH' ) ) { define( 'UPSERV_PLUGIN_PATH', plugin_dir_path( __FILE__ ) ); } @@ -63,6 +73,7 @@ if ( ! defined( 'UPSERV_MB_TO_B' ) ) { define( 'UPSERV_MB_TO_B', 1000000 ); } +// Load required files, allowing for extension via filter $require = apply_filters( 'upserv_mu_require', array( UPSERV_PLUGIN_PATH . 'autoload.php' ) ); foreach ( $require as $file ) { @@ -72,6 +83,7 @@ foreach ( $require as $file ) { } } +// Load plugin options and extract private API keys for authentication $options = json_decode( get_option( 'upserv_options' ), true ); $private_keys = array(); @@ -85,9 +97,12 @@ if ( is_array( $options ) && ! empty( $options ) && isset( $options['api'] ) ) { } } +// Initialize nonce authentication with the extracted private keys Nonce::register(); Nonce::init_auth( $private_keys ); +// Register activation, deactivation and uninstall hooks for core plugin classes +// Skip during API requests to optimize performance if ( ! Update_API::is_doing_api_request() && ! License_API::is_doing_api_request() @@ -120,12 +135,27 @@ if ( } } +// Register WP-CLI commands if the CLI environment is active if ( defined( 'WP_CLI' ) && constant( 'WP_CLI' ) ) { require_once UPSERV_PLUGIN_PATH . 'functions.php'; WP_CLI::add_command( 'updatepulse-server', 'Anyape\\UpdatePulse\\Server\\CLI\\CLI' ); } +/** + * Main initialization function for UpdatePulse Server plugin. + * + * This function is responsible for initializing different components based on request type: + * - Sets up caching + * - Loads required dependencies + * - Initializes core components like Scheduler and APIs + * - Conditionally loads admin-specific components + * + * The function intelligently loads only what's needed for each request type to optimize performance. + * + * @since 1.0.0 + * @return void + */ function upserv_run() { if ( ! did_action( 'upserv_mu_optimizer_ready' ) ) { @@ -136,11 +166,13 @@ function upserv_run() { require_once UPSERV_PLUGIN_PATH . 'functions.php'; + // Determine request type to optimize component loading $license_api_request = upserv_is_doing_license_api_request(); $priority_api_request = apply_filters( 'upserv_is_priority_api_request', $license_api_request ); $is_api_request = $priority_api_request; $objects = apply_filters( 'upserv_objects', array() ); + // Initialize core API components needed for all request types if ( ! isset( $objects['scheduler'] ) ) { $objects['scheduler'] = new Scheduler( true ); } @@ -153,12 +185,14 @@ function upserv_run() { $objects['webhook_api'] = new Webhook_API( true ); } + // Load additional components for non-priority API requests if ( ! $priority_api_request ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; do_action( 'upserv_no_priority_api_includes' ); + // Check for other API request types $is_api_request = ( upserv_is_doing_update_api_request() || upserv_is_doing_webhook_api_request() || @@ -180,6 +214,7 @@ function upserv_run() { $is_api_request = apply_filters( 'upserv_is_api_request', $is_api_request ); + // Load admin interface components only when not handling API requests if ( ! $is_api_request ) { if ( ! class_exists( 'WP_List_Table' ) ) { @@ -217,6 +252,16 @@ function upserv_run() { } add_action( 'plugins_loaded', 'upserv_run', PHP_INT_MIN + 100, 0 ); +/** + * Handles plugin updates and migrations. + * + * This function checks if we're in an API context, and if not, loads the update/migration + * functionality. During admin page loads, it hooks into plugins_loaded at a very early + * priority to ensure migrations run before the main plugin code. + * + * @since 1.0.0 + * @return void + */ function upserv_updater() { $doing_api = wp_cache_get( 'upserv_mu_doing_api', 'updatepulse-server' ); @@ -243,6 +288,7 @@ function upserv_updater() { } upserv_updater(); +// Load testing functionality if enabled via constant if ( defined( 'UPSERV_ENABLE_TEST' ) && constant( 'UPSERV_ENABLE_TEST' ) ) { if ( Update_API::is_doing_api_request() || License_API::is_doing_api_request() ) { From 81e345930d1064e67cf797b22af363d2382f76b9 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 3 Mar 2025 01:29:04 +0800 Subject: [PATCH 06/49] Full documentation WIP --- inc/api/class-license-api.php | 366 +++++++++++++++++++++++++++++++++- 1 file changed, 361 insertions(+), 5 deletions(-) diff --git a/inc/api/class-license-api.php b/inc/api/class-license-api.php index 477fa28..e073c1e 100644 --- a/inc/api/class-license-api.php +++ b/inc/api/class-license-api.php @@ -11,17 +11,65 @@ use Exception; use Anyape\UpdatePulse\Server\Server\License\License_Server; use Anyape\Utils\Utils; +/** + * License API + * + * @since 1.0.0 + */ class License_API { - protected $license_server; - protected $http_response_code = null; - protected $api_key_id; - protected $api_access; - + /** + * Is doing API request + * + * @var boolean|null + */ protected static $doing_api_request = null; + /** + * Instance + * + * @var License_API|null + */ protected static $instance; + /** + * Config + * + * @var array|null + */ protected static $config; + /** + * License server + * + * @var License_Server + */ + protected $license_server; + /** + * HTTP response code + * + * @var int|null + */ + protected $http_response_code = null; + /** + * API key ID + * + * @var string|null + */ + protected $api_key_id; + /** + * API access + * + * @var array|null + */ + protected $api_access; + + /** + * Constructor + * + * @since 1.0.0 + * + * @param boolean $init_hooks + * @param boolean $local_request + */ public function __construct( $init_hooks = false, $local_request = true ) { if ( upserv_get_option( 'use_licenses' ) ) { @@ -56,6 +104,14 @@ class License_API { // API action -------------------------------------------------- + /** + * Browse licenses + * + * @since 1.0.0 + * + * @param string $query + * @return object Result of the browse operation + */ public function browse( $query ) { $payload = json_decode( wp_unslash( $query ), true ); @@ -144,6 +200,14 @@ class License_API { return (object) $result; } + /** + * Read license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the read operation + */ public function read( $license_data ) { $result = wp_cache_get( 'upserv_license_' . md5( wp_json_encode( $license_data ) ), @@ -188,6 +252,14 @@ class License_API { return (object) $result; } + /** + * Edit license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the edit operation + */ public function edit( $license_data ) { if ( upserv_is_doing_api_request() ) { @@ -246,6 +318,14 @@ class License_API { return (object) $result; } + /** + * Add license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the add operation + */ public function add( $license_data ) { if ( $this->api_key_id ) { @@ -284,6 +364,14 @@ class License_API { return (object) $result; } + /** + * Delete license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the delete operation + */ public function delete( $license_data ) { $result = $this->license_server->delete_license( $license_data ); @@ -313,6 +401,14 @@ class License_API { return (object) $result; } + /** + * Check license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the check operation + */ public function check( $license_data ) { $license_data = apply_filters( 'upserv_check_license_dirty_payload', $license_data ); $license = $this->license_server->read_license( $license_data ); @@ -338,6 +434,14 @@ class License_API { return (object) $result; } + /** + * Activate license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the activate operation + */ public function activate( $license_data ) { $license_data = apply_filters( 'upserv_activate_license_dirty_payload', $license_data ); @@ -364,6 +468,14 @@ class License_API { return (object) $result; } + /** + * Deactivate license + * + * @since 1.0.0 + * + * @param array $license_data + * @return object Result of the deactivate operation + */ public function deactivate( $license_data ) { $license_data = apply_filters( 'upserv_deactivate_license_dirty_payload', $license_data ); @@ -392,6 +504,11 @@ class License_API { // WordPress hooks --------------------------------------------- + /** + * Add endpoints + * + * @since 1.0.0 + */ public function add_endpoints() { add_rewrite_rule( '^updatepulse-server-license-api/*$', @@ -405,6 +522,11 @@ class License_API { ); } + /** + * Parse request + * + * @since 1.0.0 + */ public function parse_request() { global $wp; @@ -415,6 +537,14 @@ class License_API { } } + /** + * Query vars filter + * + * @since 1.0.0 + * + * @param array $query_vars + * @return array The filtered query vars + */ public function query_vars( $query_vars ) { $query_vars = array_merge( $query_vars, @@ -434,6 +564,14 @@ class License_API { return $query_vars; } + /** + * Filter update request params + * + * @since 1.0.0 + * + * @param array $params + * @return array The filtered params + */ public function upserv_handle_update_request_params( $params ) { global $wp; @@ -448,6 +586,14 @@ class License_API { return $params; } + /** + * Filter license actions + * + * @since 1.0.0 + * + * @param array $actions + * @return array The filtered actions + */ public function upserv_api_license_actions( $actions ) { $actions['browse'] = __( 'Browse multiple license records', 'updatepulse-server' ); $actions['read'] = __( 'Get single license records', 'updatepulse-server' ); @@ -458,6 +604,14 @@ class License_API { return $actions; } + /** + * Filter webhook events + * + * @since 1.0.0 + * + * @param array $webhook_events + * @return array The filtered webhook events + */ public function upserv_api_webhook_events( $webhook_events ) { if ( isset( $webhook_events['license'], $webhook_events['license']['events'] ) ) { @@ -471,10 +625,24 @@ class License_API { return $webhook_events; } + /** + * Bypass license action + * + * @since 1.0.0 + */ public function upserv_bypass_did_edit_license_action() { remove_action( 'upserv_did_edit_license', array( $this, 'upserv_did_license_action' ), 20 ); } + /** + * License action + * + * @since 1.0.0 + * + * @param object $result + * @param array $payload + * @param object $original + */ public function upserv_did_license_action( $result, $payload, $original = null ) { $format = ''; $event = 'license_' . str_replace( @@ -546,6 +714,17 @@ class License_API { remove_filter( 'upserv_webhook_fire', array( $this, 'upserv_webhook_fire' ), 10 ); } + /** + * Webhook fire filter + * + * @since 1.0.0 + * + * @param boolean $fire + * @param array $payload + * @param string $url + * @param array $info + * @return boolean The filtered fire value + */ public function upserv_webhook_fire( $fire, $payload, $url, $info ) { if ( ! isset( $info['licenseAPIKey'] ) || empty( $info['licenseAPIKey'] ) ) { @@ -608,6 +787,17 @@ class License_API { return $fire; } + /** + * Fetch nonce filter + * + * @since 1.0.0 + * + * @param string $nonce + * @param string $true_nonce + * @param int $expiry + * @param array $data + * @return string|null The filterd nonce or null if invalid + */ public function upserv_fetch_nonce_private( $nonce, $true_nonce, $expiry, $data ) { $config = self::get_config(); $valid = false; @@ -647,6 +837,14 @@ class License_API { return $nonce; } + /** + * Nonce API payload filter + * + * @since 1.0.0 + * + * @param array $payload + * @return array The filtered payload + */ public function upserv_nonce_api_payload( $payload ) { global $wp; @@ -692,6 +890,13 @@ class License_API { // Misc. ------------------------------------------------------- + /** + * Is doing API request + * + * @since 1.0.0 + * + * @return boolean True if doing API request, false otherwise + */ public static function is_doing_api_request() { if ( null === self::$doing_api_request ) { @@ -701,6 +906,13 @@ class License_API { return self::$doing_api_request; } + /** + * Get config + * + * @since 1.0.0 + * + * @return array The config + */ public static function get_config() { if ( ! self::$config ) { @@ -715,6 +927,13 @@ class License_API { return apply_filters( 'upserv_license_api_config', self::$config ); } + /** + * Get instance + * + * @since 1.0.0 + * + * @return License_API The instance + */ public static function get_instance() { if ( ! self::$instance ) { @@ -724,6 +943,14 @@ class License_API { return self::$instance; } + /** + * Is package require license + * + * @since 1.0.0 + * + * @param int $package_id + * @return boolean True if package requires license, false otherwise + */ public static function is_package_require_license( $package_id ) { $require_license = wp_cache_get( 'upserv_package_require_license_' . $package_id, 'updatepulse-server', false, $found ); @@ -745,6 +972,13 @@ class License_API { * Protected methods *******************************************************************/ + /** + * Sanitize license result + * + * @since 1.0.0 + * @param object $result - by reference + * @return void + */ protected function sanitize_license_result( &$result ) { $num_allowed_domains = ( isset( $result->allowed_domains ) && @@ -767,6 +1001,14 @@ class License_API { } } + /** + * Prepare error response + * + * @param string $code + * @param string $message + * @param array $data + * @return array The response + */ protected function prepare_error_response( $code, $message, $data = array() ) { return array( 'code' => $code, @@ -775,6 +1017,12 @@ class License_API { ); } + /** + * Normalize allowed domains + * + * @param array $license_data - by reference + * @return void + */ protected function normalize_allowed_domains( &$license_data ) { if ( isset( $license_data['allowed_domains'] ) && ! is_array( $license_data['allowed_domains'] ) ) { @@ -782,6 +1030,12 @@ class License_API { } } + /** + * Extract domain from license data + * + * @param array $license_data + * @return string|false The first domain found or false if not found + */ protected function extract_domain_from_license_data( $license_data ) { if ( @@ -795,6 +1049,14 @@ class License_API { return false; } + /** + * Is valid license for state transition + * + * @param object $license + * @param string $request_slug + * @param string $domain + * @return boolean True if valid, false otherwise + */ protected function is_valid_license_for_state_transition( $license, $request_slug, $domain ) { return ( is_object( $license ) && @@ -804,6 +1066,13 @@ class License_API { ); } + /** + * Handle license activation + * + * @param object $license + * @param string $domain + * @return array|null The result or null if not found + */ protected function handle_license_activation( $license, $domain ) { $domain_count = count( $license->allowed_domains ) + 1; @@ -824,6 +1093,12 @@ class License_API { return $this->process_license_activation( $license, $domain ); } + /** + * Prepare illegal status response + * + * @param object $license + * @return array The response + */ protected function prepare_illegal_status_response( $license ) { $response = array( 'code' => 'illegal_license_status', @@ -840,6 +1115,12 @@ class License_API { return $response; } + /** + * Prepare max domains response + * + * @param object $license + * @return array The response + */ protected function prepare_max_domains_response( $license ) { return array( 'code' => 'max_domains_reached', @@ -850,6 +1131,12 @@ class License_API { ); } + /** + * Prepare already activated response + * + * @param string $domain + * @return array The response + */ protected function prepare_already_activated_response( $domain ) { return array( 'code' => 'license_already_activated', @@ -860,6 +1147,13 @@ class License_API { ); } + /** + * Process license activation + * + * @param object $license + * @param string $domain + * @return array|null The result or null if not found + */ protected function process_license_activation( $license, $domain ) { $data = isset( $license->data ) ? $license->data : array(); @@ -898,6 +1192,13 @@ class License_API { return null; } + /** + * Handle license deactivation + * + * @param object $license + * @param string $domain + * @return array|null The result or null if not found + */ protected function handle_license_deactivation( $license, $domain ) { if ( in_array( $license->status, array( 'expired', 'blocked', 'on-hold' ), true ) ) { @@ -920,6 +1221,12 @@ class License_API { return $this->process_license_deactivation( $license, $domain ); } + /** + * Prepare already deactivated response + * + * @param string $domain + * @return array The response + */ protected function prepare_already_deactivated_response( $domain ) { return array( 'code' => 'license_already_deactivated', @@ -930,6 +1237,12 @@ class License_API { ); } + /** + * Prepare too early deactivation response + * + * @param object $license + * @return array The response + */ protected function prepare_too_early_deactivation_response( $license ) { return array( 'code' => 'too_early_deactivation', @@ -940,6 +1253,13 @@ class License_API { ); } + /** + * Process license deactivation + * + * @param object $license + * @param string $domain + * @return array|null The result or null if not found + */ protected function process_license_deactivation( $license, $domain ) { $data = isset( $license->data ) ? $license->data : array(); $data['next_deactivate'] = apply_filters( @@ -980,6 +1300,13 @@ class License_API { return null; } + /** + * Handle invalid license + * + * @param array $license + * @param array $license_data + * @return array The response + */ protected function handle_invalid_license( $license, $license_data ) { if ( is_array( $license ) && isset( $license['license_not_found'] ) ) { @@ -1007,6 +1334,13 @@ class License_API { ); } + /** + * Authorize private API access + * + * @param string $action + * @param array $payload + * @return boolean True if authorized, false otherwise + */ protected function authorize_private( $action, $payload ) { $token = false; $is_auth = false; @@ -1067,6 +1401,12 @@ class License_API { return $is_auth; } + /** + * Is API public + * + * @param string $method + * @return boolean True if public, false otherwise + */ protected function is_api_public( $method ) { $public_api = apply_filters( 'upserv_license_public_api_actions', @@ -1081,6 +1421,11 @@ class License_API { return $is_api_public; } + /** + * Handle API request + * + * @since 1.0.0 + */ protected function handle_api_request() { global $wp; @@ -1164,6 +1509,13 @@ class License_API { wp_send_json( $response, $this->http_response_code, Utils::JSON_OPTIONS ); } + /** + * Authorize IP + * + * @since 1.0.0 + * + * @return boolean True if authorized, false otherwise + */ protected function authorize_ip() { $result = false; $config = self::get_config(); @@ -1185,6 +1537,10 @@ class License_API { return $result; } + /** + * Init server + * + */ protected function init_server() { $this->license_server = apply_filters( 'upserv_license_server', new License_Server() ); } From fc43a2c8ded2ce5d99f7c992986b4abc60877483 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 3 Mar 2025 07:47:32 +0800 Subject: [PATCH 07/49] More flexibility when parsing `Require License` header --- lib/anyape-package-parser/package-parser.php | 6 +++++- readme.txt | 5 ++++- updatepulse-server.php | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/anyape-package-parser/package-parser.php b/lib/anyape-package-parser/package-parser.php index 6b272b9..587265e 100644 --- a/lib/anyape-package-parser/package-parser.php +++ b/lib/anyape-package-parser/package-parser.php @@ -503,7 +503,11 @@ class Parser { $extra_headers = array(); if ( ! empty( $headers['RequireLicense'] ) ) { - $extra_headers['require_license'] = (bool) $headers['RequireLicense']; + $extra_headers['require_license'] = ! in_array( + $headers['RequireLicense'], + array( 'false', 'no', '0', 'off', 0 ), + true + ); } else { $extra_headers['require_license'] = false; } diff --git a/readme.txt b/readme.txt index e0cc03b..a60cdf6 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.3 +Stable tag: 1.0.4 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -65,6 +65,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.4 = +* More flexibility when parsing `Require License` header + = 1.0.3 = * Minor Package API fix * All API: remove `JSON_NUMERIC_CHECK` when encoding output as it creates issues with values like version numbers. diff --git a/updatepulse-server.php b/updatepulse-server.php index 3a7ff6f..e6ab044 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.3 + * Version: 1.0.4 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From e00deb45a9db90424e0e3a99ffa4485adee6e6ff Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:13:26 +0800 Subject: [PATCH 08/49] * Fix VCS test * Fix file system permission check --- inc/manager/class-package-manager.php | 8 ++++---- lib/package-update-checker/Vcs/BitbucketApi.php | 6 +++++- lib/package-update-checker/Vcs/GitHubApi.php | 5 ++++- lib/package-update-checker/Vcs/GitLabApi.php | 4 +++- readme.txt | 2 ++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/inc/manager/class-package-manager.php b/inc/manager/class-package-manager.php index 99aecf6..e13ef51 100644 --- a/inc/manager/class-package-manager.php +++ b/inc/manager/class-package-manager.php @@ -1093,6 +1093,10 @@ class Package_Manager { } public function set_package_metadata( $package_slug, $metadata ) { + WP_Filesystem(); + + global $wp_filesystem; + $dir = upserv_get_data_dir( 'metadata' ); $filename = sanitize_file_name( $package_slug . '.json' ); $file_path = trailingslashit( $dir ) . $filename; @@ -1104,10 +1108,6 @@ class Package_Manager { if ( empty( $data ) ) { if ( ! has_filter( 'upserv_did_delete_package_metadata' ) && is_file( $file_path ) ) { - WP_Filesystem(); - - global $wp_filesystem; - $result = (bool) $wp_filesystem->delete( $file_path ); } else { $result = apply_filters( 'upserv_did_delete_package_metadata', false, $package_slug ); diff --git a/lib/package-update-checker/Vcs/BitbucketApi.php b/lib/package-update-checker/Vcs/BitbucketApi.php index e512745..ec310d1 100644 --- a/lib/package-update-checker/Vcs/BitbucketApi.php +++ b/lib/package-update-checker/Vcs/BitbucketApi.php @@ -55,7 +55,11 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : return $response; } - if ( $response && isset( $response->username ) && $instance->user_name === $response->username ) { + if ( + $response && + isset( $response->username ) && + trailingslashit( $instance->user_name ) === trailingslashit( $response->username ) + ) { return true; } diff --git a/lib/package-update-checker/Vcs/GitHubApi.php b/lib/package-update-checker/Vcs/GitHubApi.php index 7c1b4bc..0526bb3 100644 --- a/lib/package-update-checker/Vcs/GitHubApi.php +++ b/lib/package-update-checker/Vcs/GitHubApi.php @@ -67,7 +67,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : return false; } - if ( isset( $response->html_url ) && $response->html_url === $url ) { + if ( + isset( $response->html_url ) && + trailingslashit( $url ) === trailingslashit( $response->html_url ) + ) { return true; } diff --git a/lib/package-update-checker/Vcs/GitLabApi.php b/lib/package-update-checker/Vcs/GitLabApi.php index 50d7413..df3dced 100644 --- a/lib/package-update-checker/Vcs/GitLabApi.php +++ b/lib/package-update-checker/Vcs/GitLabApi.php @@ -135,7 +135,9 @@ if ( ! class_exists( GitLabApi::class, false ) ) : return $response; } - return $response && isset( $response->path ) && $instance->user_name === $response->path; + return $response && + isset( $response->path ) && + trailingslashit( $instance->user_name ) === trailingslashit( $response->path ); } /** diff --git a/readme.txt b/readme.txt index a60cdf6..05f1fd5 100644 --- a/readme.txt +++ b/readme.txt @@ -67,6 +67,8 @@ This section describes how to install the plugin and get it working. = 1.0.4 = * More flexibility when parsing `Require License` header +* Fix VCS test +* Fix file system permission check = 1.0.3 = * Minor Package API fix From 8163c7baf80c19d927b50cfa4be74fdc951110af Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:23:58 +0800 Subject: [PATCH 09/49] fix deploy script --- deploy.sh | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/deploy.sh b/deploy.sh index f8cf927..67a4d87 100755 --- a/deploy.sh +++ b/deploy.sh @@ -5,7 +5,7 @@ DIR=$(pwd) PLUGINSLUG=$(basename "$DIR") MAINFILE="$PLUGINSLUG.php" # SVN user -SVNUSER=$1 +SVNUSER="" # Verbose mode VERBOSE=false # Deploy mode @@ -18,32 +18,51 @@ SCRIPT_NAME=$(basename "$0") GITBRANCH="main" # Parse arguments -for arg in "$@"; do - case "$arg" in +while [[ $# -gt 0 ]]; do + case "$1" in -d|--deploy) DEPLOY=true + shift ;; -v|--verbose) VERBOSE=true + shift ;; -sa|--skip-assets) SKIP_ASSETS=true + shift ;; -b|--branch) + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for --branch option." + exit 1 + fi GITBRANCH="$2" - shift + shift 2 ;; -mf|--mainfile) + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for --mainfile option." + exit 1 + fi MAINFILE="$2" - shift + shift 2 ;; -p|--path) + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for --path option." + exit 1 + fi DIR="$2" - shift + shift 2 ;; -s|--slug) + if [[ -z "$2" || "$2" == -* ]]; then + echo "Error: Missing value for --slug option." + exit 1 + fi PLUGINSLUG="$2" - shift + shift 2 ;; -h|--help) echo "Deploy WordPress plugin to the official repository." @@ -52,9 +71,13 @@ for arg in "$@"; do exit 0 ;; *) - # Assume the first non-flag argument is PARAM1 + # Assume the first non-flag argument is SVNUSER if [[ -z "$SVNUSER" ]]; then - SVNUSER="$arg" + SVNUSER="$1" + shift + else + echo "Error: Unexpected argument '$1'." + exit 1 fi ;; esac @@ -67,6 +90,18 @@ if [[ -z "$SVNUSER" ]]; then exit 1 fi +# Debug output (optional, for testing purposes) +if [[ "$VERBOSE" == true ]]; then + echo "SVNUSER: $SVNUSER" + echo "DEPLOY: $DEPLOY" + echo "VERBOSE: $VERBOSE" + echo "SKIP_ASSETS: $SKIP_ASSETS" + echo "GITBRANCH: $GITBRANCH" + echo "MAINFILE: $MAINFILE" + echo "DIR: $DIR" + echo "PLUGINSLUG: $PLUGINSLUG" +fi + # Git config GITPATH="$DIR/" # SVN config From 2b841124ad68e6c2ed02bf3b985a7930dbf790e8 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:07:51 +0800 Subject: [PATCH 10/49] Fix deploy file --- deploy.sh | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/deploy.sh b/deploy.sh index 67a4d87..3808559 100755 --- a/deploy.sh +++ b/deploy.sh @@ -124,7 +124,7 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" + "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } fi ;; svn) @@ -135,7 +135,7 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" + "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } fi ;; gh) @@ -146,7 +146,7 @@ execute_or_echo() { if $VERBOSE; then echo "gh ${args[*]}" fi - "$command" "${args[@]}" + "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } fi ;; *) @@ -154,7 +154,7 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" + "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } ;; esac } @@ -305,7 +305,7 @@ fi # Export HEAD of branch from git to SVN trunk execute_or_echo git checkout-index -a -f --prefix="$SVNPATH"/trunk/ -# Ignore GitHub-specific files +# Ignore files execute_or_echo svn propset svn:ignore "deploy.sh .DS_Store .vscode @@ -318,7 +318,23 @@ execute_or_echo svn add readme.txt # Create new SVN tag execute_or_echo cd "$SVNPATH" -execute_or_echo svn copy trunk/ tags/"$NEWVERSION1"/ + +# Check if the tag already exists +if svn list "$SVNURL"/tags/ | grep -q "$NEWVERSION1"; then + + # Switch back to the original branch + execute_or_echo cd "$GITPATH" + execute_or_echo git checkout "$CURRENTBRANCH" + + echo "Tag $NEWVERSION1 already exists. Exiting." + + # Clean up temporary directory + execute_or_echo rm -fr "${SVNPATH:?}/" + + exit 1 +fi + +execute_or_echo svn copy trunk tags/"$NEWVERSION1" # Add all new files in the tag folder execute_or_echo cd "$SVNPATH"/tags/"$NEWVERSION1" From b8d92631dd63ac322c7098e40325637496cd054c Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:37:48 +0800 Subject: [PATCH 11/49] Fix JSON details modal view - escaping characters --- inc/table/class-licenses-table.php | 6 +++++- inc/table/class-packages-table.php | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/inc/table/class-licenses-table.php b/inc/table/class-licenses-table.php index d5639dc..ded7149 100644 --- a/inc/table/class-licenses-table.php +++ b/inc/table/class-licenses-table.php @@ -186,7 +186,11 @@ class Licenses_Table extends WP_List_Table { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $records as $record_key => $record ) { - $bulk_value = wp_json_encode( $record ); + $bulk_value = wp_json_encode( + $record, + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP + ); $record['status_label'] = Utils::get_status_string( $record['status'] ); upserv_get_admin_template( diff --git a/inc/table/class-packages-table.php b/inc/table/class-packages-table.php index 4f49c50..e4906c6 100644 --- a/inc/table/class-packages-table.php +++ b/inc/table/class-packages-table.php @@ -179,7 +179,8 @@ class Packages_Table extends WP_List_Table { $search = ! empty( $_REQUEST['s'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $record['info'] = wp_json_encode( $info, - JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); upserv_get_admin_template( From 0e3f1c8fceda745a535a6a009e34e03b3d592b31 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:40:09 +0800 Subject: [PATCH 12/49] update readme --- readme.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.txt b/readme.txt index 05f1fd5..4a41274 100644 --- a/readme.txt +++ b/readme.txt @@ -65,6 +65,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.5 = +* Fix JSON details modal view - escaping characters + = 1.0.4 = * More flexibility when parsing `Require License` header * Fix VCS test From e7b64d6b2a30e546b41baba1ba0b289dafd43df9 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:54:29 +0800 Subject: [PATCH 13/49] set_last_update_date WIP ; must be tied to the version not just file on filesystem --- .../update/class-zip-metadata-parser.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/inc/server/update/class-zip-metadata-parser.php b/inc/server/update/class-zip-metadata-parser.php index 3900344..abea73a 100644 --- a/inc/server/update/class-zip-metadata-parser.php +++ b/inc/server/update/class-zip-metadata-parser.php @@ -162,10 +162,10 @@ class Zip_Metadata_Parser { if ( is_array( $this->package_info ) && ! empty( $this->package_info ) ) { $this->set_info_from_header(); $this->set_info_from_readme(); - $this->set_last_update_date(); $this->set_info_from_assets(); $this->set_slug(); $this->set_type(); + $this->set_last_update_date(); } else { throw new Invalid_Package_Exception( sprintf( @@ -283,10 +283,30 @@ class Zip_Metadata_Parser { /** * Add last update date to the metadata + */ + /** + * Add last update date to the metadata ; this is tied to the version */ protected function set_last_update_date() { if ( ! isset( $this->metadata['last_updated'] ) ) { + $meta = upserv_get_package_metadata( $this->slug ); + + if ( $meta && isset( $meta['version'], $meta['version_time'] ) ) { + + if ( $meta['version'] !== $this->metadata['version'] ) { + $this->metadata['last_updated'] = $meta['version_time']; + } else { + $this->metadata['last_updated'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); + $meta['version'] = $this->metadata['version']; + $meta['version_time'] = $this->metadata['last_updated']; + + upserv_set_package_metadata( $this->slug, $meta ); + } + + return; + } + $this->metadata['last_updated'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); } } From a0cc494badc3c5f6e4e72bd229bdfd7c5ae3b2eb Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:44:46 +0800 Subject: [PATCH 14/49] make sure to differenciate between file modified and last updated --- README.md | 4 +- .../update/class-zip-metadata-parser.php | 51 +++++++++++-------- inc/table/class-packages-table.php | 2 +- languages/updatepulse-server.pot | 2 +- readme.txt | 5 +- updatepulse-server.php | 2 +- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c3d7bdc..62ae54a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Package updates may be either uploaded directly, or hosted in a Version Control This plugin adds the following major features to WordPress: -* **Packages Overview:** manage package updates with a table showing Package Name, Version, Type, File Name, Size, Last Modified and License Status; includes bulk operations to delete, download and change the license status, and the ability to delete all the packages. Upload updates from your local machine to UpdatePulse Server, or let the system to automatically download them to UpdatePulse Server from a Version Control System. Store packages either locally, or in the Cloud with an S3 compatible service. Packages can also be managed through their own API. +* **Packages Overview:** manage package updates with a table showing Package Name, Version, Type, File Name, Size, File Modified and License Status; includes bulk operations to delete, download and change the license status, and the ability to delete all the packages. Upload updates from your local machine to UpdatePulse Server, or let the system to automatically download them to UpdatePulse Server from a Version Control System. Store packages either locally, or in the Cloud with an S3 compatible service. Packages can also be managed through their own API. * **Version Control Systems:** configure the Version Control Systems of your choice (Bitbucket, Github, Gitlab, or a self-hosted installation of Gitlab) with secure credentials and a branch name where the updates are hosted; choose to check for updates recurringly, or when receiving a webhook notification. UpdatePulse Server acts as a middleman between your reposiroty, your udpates storage (local or Cloud), and your clients. * **Licenses:** manage licenses with a table showing ID, License Key, Registered Email, Status, Package Type, Package Slug, Creation Date, and Expiration Date; add and edit them with a form, or use the API for more control. Licenses prevent packages installed on client machines from being updated without a valid license. Licenses are generated automatically by default and the values are unguessable (it is recommended to keep the default). When checking the validity of licenses an extra license signature is also checked to prevent the use of a license on more than the configured allowed domains. * **Not limited to WordPress:** with a platform-agnostic API, updates can be served for any type of package, not just WordPress plugins & themes. Basic examples of integration with Node.js, PHP, bash, and Python are provided in the [documentation](https://github.com/anyape/updatepulse-server/blob/main/docs/generic.md). @@ -117,7 +117,7 @@ UpdatePulse Server provides a user interface to manage packages, manage licenses ### Packages Overview This tab allows administrators to: -- View the list of packages currently available in UpdatePulse Server, with Package Name, Version, Type (Plugin or Theme), File Name, Size, Last Modified and License Status (if enabled) +- View the list of packages currently available in UpdatePulse Server, with Package Name, Version, Type (Plugin or Theme), File Name, Size, File Modified (when the package file was changed on the file system) and License Status (if enabled) - Download a package - Delete a package - Apply bulk actions on the list of packages (download, delete) diff --git a/inc/server/update/class-zip-metadata-parser.php b/inc/server/update/class-zip-metadata-parser.php index abea73a..cd537f8 100644 --- a/inc/server/update/class-zip-metadata-parser.php +++ b/inc/server/update/class-zip-metadata-parser.php @@ -282,33 +282,40 @@ class Zip_Metadata_Parser { } /** - * Add last update date to the metadata - */ - /** * Add last update date to the metadata ; this is tied to the version */ protected function set_last_update_date() { - if ( ! isset( $this->metadata['last_updated'] ) ) { - $meta = upserv_get_package_metadata( $this->slug ); - - if ( $meta && isset( $meta['version'], $meta['version_time'] ) ) { - - if ( $meta['version'] !== $this->metadata['version'] ) { - $this->metadata['last_updated'] = $meta['version_time']; - } else { - $this->metadata['last_updated'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); - $meta['version'] = $this->metadata['version']; - $meta['version_time'] = $this->metadata['last_updated']; - - upserv_set_package_metadata( $this->slug, $meta ); - } - - return; - } - - $this->metadata['last_updated'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); + if ( isset( $this->metadata['last_updated'] ) ) { + return; } + + $meta = upserv_get_package_metadata( $this->slug ); + + if ( ! is_array( $meta ) ) { + $meta = array(); + } + + php_log( + array( + 'version_time' => isset( $meta['version_time'] ) ? $meta['version_time'] : '', + 'version' => isset( $meta['version'] ) ? $meta['version'] : '', + ), + ); + + if ( + ! isset( $meta['version'], $meta['version_time'] ) || + $meta['version'] !== $this->metadata['version'] + ) { + $meta['version'] = $this->metadata['version']; + $meta['version_time'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); + + php_log( 'updated version to ' . $meta['version'] . ' at ' . $meta['version_time'] ); + + upserv_set_package_metadata( $this->slug, $meta ); + } + + $this->metadata['last_updated'] = $meta['version_time']; } protected function set_type() { diff --git a/inc/table/class-packages-table.php b/inc/table/class-packages-table.php index e4906c6..b2c5689 100644 --- a/inc/table/class-packages-table.php +++ b/inc/table/class-packages-table.php @@ -46,7 +46,7 @@ class Packages_Table extends WP_List_Table { 'col_type' => __( 'Type', 'updatepulse-server' ), 'col_file_name' => __( 'File Name', 'updatepulse-server' ), 'col_file_size' => __( 'Size', 'updatepulse-server' ), - 'col_file_last_modified' => __( 'Last Modified ', 'updatepulse-server' ), + 'col_file_last_modified' => __( 'File Modified ', 'updatepulse-server' ), 'col_origin' => __( 'Origin', 'updatepulse-server' ), ) ); diff --git a/languages/updatepulse-server.pot b/languages/updatepulse-server.pot index c5ba9da..8ea7eb5 100644 --- a/languages/updatepulse-server.pot +++ b/languages/updatepulse-server.pot @@ -1074,7 +1074,7 @@ msgid "" msgstr "" #: inc/table/class-packages-table.php:49 -msgid "Last Modified " +msgid "File Modified " msgstr "" #: inc/templates/admin/plugin-remote-sources-page.php:198 diff --git a/readme.txt b/readme.txt index 4a41274..8c06004 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.4 +Stable tag: 1.0.5 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -37,7 +37,7 @@ Make sure to read the full documentation and the content of the "Help" tab under This plugin adds the following major features to WordPress: -* **Package management:** to manage update packages, showing a listing with Package Name, Version, Type, File Name, Size, Last Modified and License Status; includes bulk operations to delete and download, and the ability to delete all the packages. +* **Package management:** to manage update packages, showing a listing with Package Name, Version, Type, File Name, Size, File Modified and License Status; includes bulk operations to delete and download, and the ability to delete all the packages. * **Add Packages:** Upload update packages from a local machine to the server, or download them to the server from a Version Control System. * **Version Control Systems:** Instead of manually uploading packages, use Version Control Systems to host packages, and download them to UpdatePulse Server automatically. Supports Bitbucket, Github and Gitlab, as well as self-hosted installations of Gitlab. * **Cloud Storage**: Instead of storing packages on the file system where UpdatePulse Server is installed, they can be stored on a cloud storage service, as long as it is compatible with Amazon S3's API. Examples: Amazon S3, Cloudflare R2, Backblaze B2, MinIO, and many more! @@ -67,6 +67,7 @@ This section describes how to install the plugin and get it working. = 1.0.5 = * Fix JSON details modal view - escaping characters +* Make sure to differenciate between `file_last_modified` ("File Modified", the time the file was changed on the file system) and `last_updated` (package version update time) = 1.0.4 = * More flexibility when parsing `Require License` header diff --git a/updatepulse-server.php b/updatepulse-server.php index e6ab044..2d7b170 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.4 + * Version: 1.0.5 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From 2b5b31ac591722ca4f9ea2803c7073fcd1b638c7 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:45:44 +0800 Subject: [PATCH 15/49] cleanup --- inc/server/update/class-zip-metadata-parser.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/inc/server/update/class-zip-metadata-parser.php b/inc/server/update/class-zip-metadata-parser.php index cd537f8..fbab5d0 100644 --- a/inc/server/update/class-zip-metadata-parser.php +++ b/inc/server/update/class-zip-metadata-parser.php @@ -296,13 +296,6 @@ class Zip_Metadata_Parser { $meta = array(); } - php_log( - array( - 'version_time' => isset( $meta['version_time'] ) ? $meta['version_time'] : '', - 'version' => isset( $meta['version'] ) ? $meta['version'] : '', - ), - ); - if ( ! isset( $meta['version'], $meta['version_time'] ) || $meta['version'] !== $this->metadata['version'] @@ -310,8 +303,6 @@ class Zip_Metadata_Parser { $meta['version'] = $this->metadata['version']; $meta['version_time'] = gmdate( 'Y-m-d H:i:s', filemtime( $this->filename ) ); - php_log( 'updated version to ' . $meta['version'] . ' at ' . $meta['version_time'] ); - upserv_set_package_metadata( $this->slug, $meta ); } From 78611f86e4cfbbcde74402e4790359d211e0bdda Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:52:31 +0800 Subject: [PATCH 16/49] 1.0.5 --- deploy.sh | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index 3808559..74250e4 100755 --- a/deploy.sh +++ b/deploy.sh @@ -108,6 +108,14 @@ GITPATH="$DIR/" SVNPATH="/tmp/$PLUGINSLUG" # path to a temp SVN repo. No trailing slash required and don't add trunk. SVNURL="http://plugins.svn.wordpress.org/$PLUGINSLUG/" # Remote SVN repo on wordpress.org, with no trailing slash +# Function to handle command errors +handle_error() { + local cmd="$1" + local exit_code="$2" + echo "Error: $cmd command failed with exit code $exit_code" + exit 1 +} + # Function to execute or echo commands based on deploy mode execute_or_echo() { local command="$1" @@ -124,7 +132,11 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } + "$command" "${args[@]}" + local exit_code=$? + if [[ $exit_code -ne 0 ]]; then + handle_error "$command" "$exit_code" + fi fi ;; svn) @@ -135,7 +147,11 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } + "$command" "${args[@]}" + local exit_code=$? + if [[ $exit_code -ne 0 ]]; then + handle_error "$command" "$exit_code" + fi fi ;; gh) @@ -146,7 +162,11 @@ execute_or_echo() { if $VERBOSE; then echo "gh ${args[*]}" fi - "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } + "$command" "${args[@]}" + local exit_code=$? + if [[ $exit_code -ne 0 ]]; then + handle_error "$command" "$exit_code" + fi fi ;; *) @@ -154,7 +174,11 @@ execute_or_echo() { if $VERBOSE; then echo "$command ${args[*]}" fi - "$command" "${args[@]}" || { echo "Error: $command command failed"; exit 1; } + "$command" "${args[@]}" + local exit_code=$? + if [[ $exit_code -ne 0 ]]; then + handle_error "$command" "$exit_code" + fi ;; esac } From 909c91e02e6f0332d376176feb9116468918eb30 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:59:59 +0800 Subject: [PATCH 17/49] enhance deploy script --- deploy.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index 74250e4..0f580c9 100755 --- a/deploy.sh +++ b/deploy.sh @@ -135,7 +135,7 @@ execute_or_echo() { "$command" "${args[@]}" local exit_code=$? if [[ $exit_code -ne 0 ]]; then - handle_error "$command" "$exit_code" + handle_error "$command ${args[*]}" fi fi ;; @@ -150,7 +150,7 @@ execute_or_echo() { "$command" "${args[@]}" local exit_code=$? if [[ $exit_code -ne 0 ]]; then - handle_error "$command" "$exit_code" + handle_error "$command ${args[*]}" fi fi ;; @@ -165,7 +165,7 @@ execute_or_echo() { "$command" "${args[@]}" local exit_code=$? if [[ $exit_code -ne 0 ]]; then - handle_error "$command" "$exit_code" + handle_error "$command ${args[*]}" fi fi ;; @@ -177,7 +177,7 @@ execute_or_echo() { "$command" "${args[@]}" local exit_code=$? if [[ $exit_code -ne 0 ]]; then - handle_error "$command" "$exit_code" + handle_error "$command ${args[*]}" fi ;; esac From be55f7bb9fe98797db7a9362c542d9c7c91d53bf Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:02:51 +0800 Subject: [PATCH 18/49] Make exception for git commit when there's nothing to commit --- deploy.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 0f580c9..1f835e5 100755 --- a/deploy.sh +++ b/deploy.sh @@ -135,7 +135,15 @@ execute_or_echo() { "$command" "${args[@]}" local exit_code=$? if [[ $exit_code -ne 0 ]]; then - handle_error "$command ${args[*]}" + # Make exception for git commit when there's nothing to commit + if [[ "$command" == "git" && "${args[0]}" == "commit" && $exit_code -eq 1 ]]; then + # Check if the error is about "nothing to commit" + if git status | grep -q "nothing to commit"; then + echo "Notice: Nothing to commit, continuing with deployment" + return 0 + fi + fi + handle_error "$command ${args[*]}" "$exit_code" fi fi ;; From 362ccb664063d7cb9e1a7f4e0a9019dc23c474c5 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:07:29 +0800 Subject: [PATCH 19/49] handle errors in the handler xD --- deploy.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/deploy.sh b/deploy.sh index 1f835e5..5a1d442 100755 --- a/deploy.sh +++ b/deploy.sh @@ -113,6 +113,11 @@ handle_error() { local cmd="$1" local exit_code="$2" echo "Error: $cmd command failed with exit code $exit_code" + cd "$GITPATH" || { + echo "Error: Unable to change directory to $GITPATH" + exit 1 + } + git checkout "$CURRENTBRANCH" exit 1 } From df4ca42486ee34807b9ab9c7a189999fa55255ea Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:15:58 +0800 Subject: [PATCH 20/49] enhanced deploy script --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 5a1d442..f222bac 100755 --- a/deploy.sh +++ b/deploy.sh @@ -343,7 +343,7 @@ fi execute_or_echo git checkout-index -a -f --prefix="$SVNPATH"/trunk/ # Ignore files -execute_or_echo svn propset svn:ignore "deploy.sh +execute_or_echo svn propset svn:ignore "*.sh .DS_Store .vscode .git From 03398f03a26aade709fa255c968b0a8a8af2577c Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sat, 15 Mar 2025 12:04:01 +0800 Subject: [PATCH 21/49] Fix webhook payload handling & version bump --- inc/api/class-webhook-api.php | 3 +-- readme.txt | 5 ++++- updatepulse-server.php | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index d65b532..0a0b8b4 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -293,7 +293,6 @@ class Webhook_API { $delay = $vcs_config['check_delay']; $dir = Data_Manager::get_data_dir( 'packages' ); $package_exists = null; - $payload = $payload ? wp_json_encode( $payload ) : false; $package_exists = apply_filters( 'upserv_webhook_package_exists', $package_exists, @@ -480,7 +479,7 @@ class Webhook_API { } } - return $decoded; + return ! is_array( $decoded ) ? array( 'decoded' => $decoded ) : $decoded; } protected function get_payload_vcs_url( $payload ) { diff --git a/readme.txt b/readme.txt index 8c06004..c756c05 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.5 +Stable tag: 1.0.6 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -65,6 +65,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.6 = +* Fix webhook payload handling (thanks @eHtmlu on github) + = 1.0.5 = * Fix JSON details modal view - escaping characters * Make sure to differenciate between `file_last_modified` ("File Modified", the time the file was changed on the file system) and `last_updated` (package version update time) diff --git a/updatepulse-server.php b/updatepulse-server.php index 2d7b170..ec7fbd5 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.5 + * Version: 1.0.6 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From 8f02af5c90fe4e243bee66249fdc67714e95b099 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sun, 16 Mar 2025 12:11:44 +0800 Subject: [PATCH 22/49] Readme Troubleshooting and FAQ sections ; Update API documentation --- docs/misc.md | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ readme.txt | 54 ++++++++++++++++ 2 files changed, 225 insertions(+) diff --git a/docs/misc.md b/docs/misc.md index edecdbc..cc6fc7e 100644 --- a/docs/misc.md +++ b/docs/misc.md @@ -8,6 +8,9 @@ UpdatePulse Server provides an API and offers a series of functions, actions and * [Acquiring a reusable token or a true nonce - payload](#acquiring-a-reusable-token-or-a-true-nonce---payload) * [Responses](#responses) * [Building API credentials and API signature](#building-api-credentials-and-api-signature) + * [Update API](#update-api) + * [The `get_metadata` action](#the-get_metadata-action) + * [The `download` action](#the-download-action) * [WP CLI](#wp-cli) * [Consuming Webhooks](#consuming-webhooks) * [Functions](#functions) @@ -248,6 +251,174 @@ echo '
The credentials are: ' . esc_html( $values['credentials'] ) . '
echo '
The signature is: ' . esc_html( $values['signature'] ) . '
'; ``` +## Update API + +The Update API is accessible via `GET` requests on the `/updatepulse-server-update-api/` endpoint. +It has two actions: `get_metadata` and `download`. + +### The `get_metadata` action + +The `get_metadata` action is used to check for updates. It accepts the following parameters: + +| Parameter | Description | Required | +| --- | --- | --- | +| `action` | The action to perform. Must be `get_metadata`. | Yes | +| `package_id` | The ID of the package to check for updates. | Yes | +| `installed_version` | The version of the package currently installed. | No | +| `php` | The PHP version of the client. | No | +| `locale` | The locale of the client. | No | +| `checking_for_updates` | A flag indicating whether the client is checking for updates. | No | +| `license_key` | The license key of the package | Yes (if the package requires a license) | +| `license_signature` | The license signature of the package | Yes (if the package requires a license) | +| `update_type` | The type of update. Must be one of `Plugin`, `Theme`, or `Generic`. | Yes | + +Example of a request to the Update API with: +- `get_metadata` action +- `package_id` set to `dummy-plugin` +- `installed_version` set to `1.0` +- `php` set to `8.3` +- `locale` set to `en_US` +- `checking_for_updates` set to `1` +- `license_key` set to `abcdef1234567890` +- `license_signature` set to `signabcdef1234567890` +- `update_type` set to `Plugin` + +```bash +curl -X GET "https://server.domain.tld/updatepulse-server-update-api/?action=get_metadata&package_id=dummy-plugin&installed_version=1.0&php=8.3&locale=en_US&checking_for_updates=1&license_key=abcdef1234567890&license_signature=signabcdef1234567890&update_type=Plugin" +``` + +Example of a response (success): +```json +{ + "name": "Dummy Plugin", + "version": "1.5.0", + "homepage": "https:\/\/domain.tld\/", + "author": "A Developer", + "author_homepage": "https:\/\/domain.tld\/", + "description": "Updated Empty plugin to demonstrate the UpdatePulse Updater.", + "details_url": "https:\/\/domain.tld\/", + "requires": "4.9.8", + "tested": "4.9.8", + "requires_php": "7.0", + "sections": { + "description": "

Update Plugin description. Basic HTML<\/strong> can be used in all sections.<\/p><\/div>", + "dummy_section": "

An extra, dummy section.<\/p><\/div>", + "installation": "

Installation instructions.<\/p><\/div>", + "changelog": "

This section will be displayed by default when the user clicks 'View version x.y.z details'.<\/p><\/div>", + "frequently_asked_questions": "

Question<\/h4>

Answer<\/p><\/div>", + }, + "icons": { + "1x": "https:\/\/domain.tld\/path\/to\/icon-128x128.png", + "2x": "https:\/\/domain.tld\/path\/to\/icon-256x256.png", + }, + "banners": { + "low": "https:\/\/domain.tld\/path\/to\/banner-772x250.png", + "high": "https:\/\/domain.tld\/path\/to\/banner-1544x500.png", + }, + "require_license": "1", + "slug": "dummy-plugin", + "type": "plugin", + "download_url": "https:\/\/server.domain.tld\/updatepulse-server-update-api\/?action=download&package_id=dummy-plugin&token=tokenabcdef1234567890&license_key=abcdef1234567890&license_signature=signabcdef1234567890&update_type=Plugin", + "license": { + "license_key": "abcdef1234567890", + "max_allowed_domains": 2, + "allowed_domains": [ + "domain.tld", + "domain2.tld" + ], + "status": "activated", + "txn_id": "", + "date_created": "2025-02-04", + "date_renewed": "0000-00-00", + "date_expiry": "2027-02-04", + "package_slug": "dummy-plugin", + "package_type": "plugin", + "result": "success", + "message": "License key details retrieved." + }, + "time_elapsed": "0.139s" +} +``` + +Examples of a response (failure - invalid package): +```json +{ + "error": "no_server", + "message": "No server found for this package." +} +``` + +Examples of a response (failure - invalid license): +```json +{ + "name": "Dummy Plugin", + "version": "1.5.0", + "homepage": "https:\/\/domain.tld\/", + "author": "A Developer", + "author_homepage": "https:\/\/domain.tld\/", + "description": "Updated Empty plugin to demonstrate the UpdatePulse Updater.", + "details_url": "https:\/\/domain.tld\/", + "requires": "4.9.8", + "tested": "4.9.8", + "requires_php": "7.0", + "sections": { + "description": "

Update Plugin description. Basic HTML<\/strong> can be used in all sections.<\/p><\/div>", + "dummy_section": "

An extra, dummy section.<\/p><\/div>", + "installation": "

Installation instructions.<\/p><\/div>", + "changelog": "

This section will be displayed by default when the user clicks 'View version x.y.z details'.<\/p><\/div>", + "frequently_asked_questions": "

Question<\/h4>

Answer<\/p><\/div>", + }, + "icons": { + "1x": "https:\/\/domain.tld\/path\/to\/icon-128x128.png", + "2x": "https:\/\/domain.tld\/path\/to\/icon-256x256.png", + }, + "banners": { + "low": "https:\/\/domain.tld\/path\/to\/banner-772x250.png", + "high": "https:\/\/domain.tld\/path\/to\/banner-1544x500.png", + }, + "require_license": "1", + "slug": "dummy-plugin", + "type": "plugin", + "license_error": { + "code": "invalid_license", + "message": "The license key or signature is invalid.", + "data": { + "license": false + } + }, + "time_elapsed": "0.139s" +} +``` + +### The `download` action + +The `download` action is used to download the package. It accepts the following parameters: + +| Parameter | Description | Required | +| --- | --- | --- | +| `action` | The action to perform. Must be `download`. | Yes | +| `package_id` | The ID of the package to download. | Yes | +| `token` | The cryptographic token to use to download the package. Generated by the Nonce API. | Yes | +| `license_key` | The license key of the package | Yes (if the package requires a license) | +| `license_signature` | The license signature of the package | Yes (if the package requires a license) | +| `update_type` | The type of update. Must be one of `Plugin`, `Theme`, or `Generic`. | Yes | + +Generally, the URL to request this API endpoint would not be put together manually, but rather taken from the field `download_url` in the response of `get_metadata` action. + +Example of a request to the Update API with: +- `download` action +- `package_id` set to `dummy-plugin` +- `token` set to `tokenabcdef1234567890` +- `license_key` set to `abcdef1234567890` +- `license_signature` set to `signabcdef1234567890` +- `update_type` set to `Plugin` + +```bash +curl -X GET "https://server.domain.tld/updatepulse-server-update-api/?action=download&package_id=dummy-plugin&token=tokenabcdef1234567890&license_key=abcdef1234567890&license_signature=signabcdef1234567890&update_type=Plugin" +``` + +The response is a `zip` file containing the package. + ## WP CLI UpdatePulse Server provides a series of commands to interact with the plugin: diff --git a/readme.txt b/readme.txt index c756c05..49ef056 100644 --- a/readme.txt +++ b/readme.txt @@ -47,6 +47,60 @@ This plugin adds the following major features to WordPress: To connect their plugins or themes and UpdatePulse Server, developers can find integration examples in the [UpdatePulse Server Integration Examples](https://github.com/Anyape/updatepulse-server-integration) repository - theme and plugin examples rely heavily on the popular [Plugin Update Checker](https://github.com/YahnisElsts/plugin-update-checker) by [Yahnis Elsts](https://github.com/YahnisElsts). +== Troubleshooting == + +Please read the plugin FAQ, there is a lot that may help you there! + +UpdatePulse Server is regularly updated for compatibility, and bug reports are welcome, preferably on [Github](https://github.com/anyape/updatepulse-server/). Pull Requests from developers following the [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) (`WordPress-Extra` ruleset) are highly appreciated and will be credited upon merge. + +In case the plugin has not been updated for a while, no panic: it simply means the compatibility flag has not been changed, and it very likely remains compatible with the latest version of WordPress. This is because it was designed with long-term compatibility in mind from the ground up. + +Each **bug** report will be addressed in a timely manner if properly documented – previously unanswered general inquiries and issues reported on the WordPress forum may take significantly longer to receive a response (if any). + +**Only issues occurring with WordPress core, WooCommerce, and default WordPress themes (incl. WooCommerce Storefront) will be considered.** + +**Troubleshooting involving 3rd-party plugins or themes will not be addressed on the WordPress support forum.** + +== FAQ == + += How do I use UpdatePulse Server? = +UpdatePulse Server is a plugin for developers, not end-users. It allows developers to provide updates for their software packages, including WordPress plugins and themes. For more information on how to use it, please refer to the [documentation](https://github.com/anyape/updatepulse-server/blob/main/README.md). + += How do I connect my plugin/theme to UpdatePulse Server? = +To connect your plugin or theme to UpdatePulse Server, you can either use one of the integration examples provided in the [UpdatePulse Server Integration Examples](https://github.com/Anyape/updatepulse-server-integration), or develop your own on top of [Plugin Update Checker](https://github.com/YahnisElsts/plugin-update-checker). + +If you decide to develop your own, the key is to call the [UpdatePulse Server Update API](https://github.com/anyape/updatepulse-server/blob/main/docs/misc.md#update-api) to check for updates, with the necessary information in the request. The API will return a JSON response with the update information, which you can then use to display the update notification, check for a license for your plugin or theme, and download the update package. + += How does the license system work? = +The license system allows developers to manage licenses for their software packages. Licenses prevent packages from being updated without a valid license. License Keys are generated automatically by default and the values are unguessable (it is recommended to keep the default). When checking the validity of licenses, an extra license signature is also checked to prevent the use of a license on more than the configured allowed domains. + += How do I manage packages? = +You can manage packages through the UpdatePulse Server interface, through the API, or by letting the plugin download them automatically from a Version Control System (preferred). The interface allows you to view a listing of packages, view details, delete, download, and upload new packages manually (discouraged). + += I have a problem with the plugin, what should I do? = +If you have a problem with the plugin, please check the FAQ and the documentation first. + +Then, make sure to flush your WordPress permalinks (Settings > Permalinks > Save Changes), clear your browser cache, and clear any caching plugins you may have installed. If you are using a CDN, make sure to clear the cache there as well. + +Make sure you are not trying to update a package installed alongside UpdatePulse Server - the package must be installed on a different WordPress installation. + +If you still have a problem, please open an issue on [GitHub](https://github.com/Anyape/updatepulse-server/issues) with a **detailed description of the problem**, including any **error messages you are receiving**, and **most importantly, the steps to reproduce the issue, in details**. + +Only issues occurring with WordPress core, WooCommerce, and default WordPress themes (incl. WooCommerce Storefront) will be considered: integration with 3rd-party plugins or themes will only be addressed if you can provide a patch in a pull request, and if this makes sense for the author. If not, please either contact the author of the plugin/theme you are having issues with, or provide your own integration with a custom plugin. + += How can I sell package licenses? = +UpdatePulse Server does not provide a built-in way to sell licenses. To sell licenses, your chosen e-commerce solution must be integrated with UpdatePulse Server License API. This can be done by creating a custom plugin that connects your e-commerce solution with UpdatePulse Server License API, or by using an existing integration if available. At this time, there is no official e-commerce integration plugin for UpdatePulse Server. + += Is UpdatePulse Server compatible with X Plugin/Theme? with multisite? = + +UpdatePulse Server by itself does not provide any frontend functionality to your users. + +As a general rule, the more isolated UpdatePulse Server is from the rest of your ecosystem, the better, as it allows the server to perform without interference: it is not meant to be used alongside other plugins or themes, but more as a standalone server. + +UpdatePulse Server is not meant to be used in a multisite environment either: it is a server delivering packages and licenses to clients, and has no place in a multisite environment. + +If you still decide to use UpdatePulse Server on a website not solely dedicated to it, it is still possible ; to avoid interference, you may want to add the MU Plugin `upserv-plugins-optimizer.php` provided in the [UpdatePulse Server Integration](https://github.com/Anyape/updatepulse-server-integration) repository to bypass plugins and themes when calling the UpdatePulse Server APIs. + == Screenshots == 1. Packages Overview From 28c10e9e459ac27aac2d909b53255f7c3ea86dbb Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:51:12 +0800 Subject: [PATCH 23/49] Full documentation WIP --- inc/api/class-license-api.php | 120 +++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/inc/api/class-license-api.php b/inc/api/class-license-api.php index e073c1e..c864f9d 100644 --- a/inc/api/class-license-api.php +++ b/inc/api/class-license-api.php @@ -410,6 +410,11 @@ class License_API { * @return object Result of the check operation */ public function check( $license_data ) { + /** + * Filter the license data payload before checking a license. + * + * @param array $license_data The license data payload. + */ $license_data = apply_filters( 'upserv_check_license_dirty_payload', $license_data ); $license = $this->license_server->read_license( $license_data ); $raw_result = array(); @@ -423,8 +428,19 @@ class License_API { $result = null; } + /** + * Filter the result of the license check operation. + * + * @param object|null $license The license object or null if not found. + * @param array $license_data The license data payload. + */ $result = apply_filters( 'upserv_check_license_result', $license, $license_data ); + /** + * Fired after checking a license. + * + * @param mixed $raw_result The raw result of the license check. + */ do_action( 'upserv_did_check_license', $raw_result ); if ( ! is_object( $result ) ) { @@ -443,6 +459,11 @@ class License_API { * @return object Result of the activate operation */ public function activate( $license_data ) { + /** + * Filter the license data payload before activating a license. + * + * @param array $license_data The license data payload. + */ $license_data = apply_filters( 'upserv_activate_license_dirty_payload', $license_data ); $this->normalize_allowed_domains( $license_data ); @@ -451,6 +472,11 @@ class License_API { $license = $this->license_server->read_license( $license_data ); $domain = $this->extract_domain_from_license_data( $license_data ); + /** + * Fired before activating a license. + * + * @param object $license The license object. + */ do_action( 'upserv_pre_activate_license', $license ); if ( $this->is_valid_license_for_state_transition( $license, $request_slug, $domain ) ) { @@ -461,8 +487,22 @@ class License_API { $raw_result = isset( $result['raw_result'] ) ? $result['raw_result'] : $result; $result = isset( $result['result'] ) ? $result['result'] : $result; - $result = apply_filters( 'upserv_activate_license_result', $result, $license_data, $license ); + /** + * Filter the result of the license activation operation. + * + * @param object $result The result of the license activation. + * @param array $license_data The license data payload. + * @param object $license The license object. + */ + $result = apply_filters( 'upserv_activate_license_result', $result, $license_data, $license ); + + /** + * Fired after activating a license. + * + * @param mixed $raw_result The raw result of the license activation. + * @param array $license_data The license data payload. + */ do_action( 'upserv_did_activate_license', $raw_result, $license_data ); return (object) $result; @@ -477,6 +517,11 @@ class License_API { * @return object Result of the deactivate operation */ public function deactivate( $license_data ) { + /** + * Filter the license data payload before deactivating a license. + * + * @param array $license_data The license data payload. + */ $license_data = apply_filters( 'upserv_deactivate_license_dirty_payload', $license_data ); $this->normalize_allowed_domains( $license_data ); @@ -485,6 +530,11 @@ class License_API { $license = $this->license_server->read_license( $license_data ); $domain = $this->extract_domain_from_license_data( $license_data ); + /** + * Fired before deactivating a license. + * + * @param object $license The license object. + */ do_action( 'upserv_pre_deactivate_license', $license ); if ( $this->is_valid_license_for_state_transition( $license, $request_slug, $domain ) ) { @@ -495,8 +545,22 @@ class License_API { $raw_result = isset( $result['raw_result'] ) ? $result['raw_result'] : $result; $result = isset( $result['result'] ) ? $result['result'] : $result; - $result = apply_filters( 'upserv_deactivate_license_result', $result, $license_data, $license ); + /** + * Filter the result of the license deactivation operation. + * + * @param object $result The result of the license deactivation. + * @param array $license_data The license data payload. + * @param object $license The license object. + */ + $result = apply_filters( 'upserv_deactivate_license_result', $result, $license_data, $license ); + + /** + * Fired after deactivating a license. + * + * @param mixed $raw_result The raw result of the license deactivation. + * @param array $license_data The license data payload. + */ do_action( 'upserv_did_deactivate_license', $raw_result, $license_data ); return (object) $result; @@ -924,6 +988,11 @@ class License_API { self::$config = $config; } + /** + * Filter the License API configuration. + * + * @param array $config The License API configuration. + */ return apply_filters( 'upserv_license_api_config', self::$config ); } @@ -1158,6 +1227,12 @@ class License_API { $data = isset( $license->data ) ? $license->data : array(); if ( ! isset( $data['next_deactivate'] ) || time() > $data['next_deactivate'] ) { + /** + * Filter the timestamp for the next allowed deactivation after activation. + * + * @param int $timestamp The timestamp for the next allowed deactivation. + * @param object $license The license object. + */ $data['next_deactivate'] = apply_filters( 'upserv_activate_license_next_deactivate', time(), $license ); } @@ -1170,6 +1245,11 @@ class License_API { try { $result = $this->license_server->edit_license( + /** + * Filter the payload for license activation. + * + * @param array $payload The license activation payload. + */ apply_filters( 'upserv_activate_license_payload', $payload ) ); } catch ( Exception $e ) { @@ -1261,7 +1341,13 @@ class License_API { * @return array|null The result or null if not found */ protected function process_license_deactivation( $license, $domain ) { - $data = isset( $license->data ) ? $license->data : array(); + $data = isset( $license->data ) ? $license->data : array(); + /** + * Filter the timestamp for the next allowed deactivation. + * + * @param int $timestamp The timestamp for the next allowed deactivation. + * @param object $license The license object. + */ $data['next_deactivate'] = apply_filters( 'upserv_deactivate_license_next_deactivate', (bool) ( constant( 'WP_DEBUG' ) ) ? time() + ( MINUTE_IN_SECONDS / 4 ) : time() + MONTH_IN_SECONDS, @@ -1278,6 +1364,11 @@ class License_API { try { $result = $this->license_server->edit_license( + /** + * Filter the payload for license deactivation. + * + * @param array $payload The license deactivation payload. + */ apply_filters( 'upserv_activate_license_payload', $payload ) ); } catch ( Exception $e ) { @@ -1408,6 +1499,11 @@ class License_API { * @return boolean True if public, false otherwise */ protected function is_api_public( $method ) { + /** + * Filter the list of public License API actions. + * + * @param array $public_api_actions List of public License API actions. + */ $public_api = apply_filters( 'upserv_license_public_api_actions', array( @@ -1461,6 +1557,13 @@ class License_API { if ( ! $malformed_request ) { $this->init_server(); + /** + * Filter whether the License API request is authorized. + * + * @param bool $authorized Whether the License API request is authorized. + * @param string $method The method of the request. + * @param array $payload The payload of the request. + */ $authorized = apply_filters( 'upserv_license_api_request_authorized', ( @@ -1475,6 +1578,12 @@ class License_API { ); if ( $authorized ) { + /** + * Fired before the License API request is processed. + * + * @param string $method The License API action. + * @param array $payload The payload of the request. + */ do_action( 'upserv_license_api_request', $method, $payload ); if ( method_exists( $this, $method ) ) { @@ -1542,6 +1651,11 @@ class License_API { * */ protected function init_server() { + /** + * Filter the License Server instance. + * + * @param License_Server $license_server The license server instance. + */ $this->license_server = apply_filters( 'upserv_license_server', new License_Server() ); } } From 76de3434c5c19678edbe9a57b22f793e619a027f Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:58:31 +0800 Subject: [PATCH 24/49] Full documentation WIP --- inc/api/class-package-api.php | 60 +++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/inc/api/class-package-api.php b/inc/api/class-package-api.php index 527a0e8..ef06be3 100644 --- a/inc/api/class-package-api.php +++ b/inc/api/class-package-api.php @@ -54,7 +54,14 @@ class Package_API { $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; - $result = apply_filters( 'upserv_package_browse', $result, $query ); + /** + * 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 + */ + $result = apply_filters( 'upserv_package_browse', $result, $query ); do_action( 'upserv_did_browse_package', $result ); @@ -86,6 +93,14 @@ class Package_API { 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 + */ $result = apply_filters( 'upserv_package_read', $result, $package_id, $type ); do_action( 'upserv_did_read_package', $result ); @@ -118,6 +133,14 @@ class Package_API { $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 + */ $result = apply_filters( 'upserv_package_edit', $result, $package_id, $type ); if ( empty( $exists ) ) { @@ -162,6 +185,14 @@ class Package_API { $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 + */ $result = apply_filters( 'upserv_package_add', $result, $package_id, $type ); if ( ! empty( $exists ) ) { @@ -193,6 +224,14 @@ class Package_API { 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 + */ $result = apply_filters( 'upserv_package_delete', $result, $package_id, $type ); if ( $result ) { @@ -230,7 +269,15 @@ class Package_API { public function signed_url( $package_id, $type ) { $package_id = filter_var( $package_id, FILTER_SANITIZE_URL ); $type = filter_var( $type, FILTER_SANITIZE_URL ); - $token = apply_filters( 'upserv_package_signed_url_token', false, $package_id, $type ); + /** + * 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 + */ + $token = apply_filters( 'upserv_package_signed_url_token', false, $package_id, $type ); if ( ! $token ) { $token = upserv_create_nonce( @@ -244,6 +291,14 @@ class Package_API { ); } + /** + * 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 + */ $result = apply_filters( 'upserv_package_signed_url', array( @@ -282,7 +337,6 @@ class Package_API { '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&', From db07e75b7dddb0ed46a95ed0372858daed369a1b Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:28:01 +0800 Subject: [PATCH 25/49] Full documentation WIP --- inc/api/class-package-api.php | 121 ++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/inc/api/class-package-api.php b/inc/api/class-package-api.php index ef06be3..a32caff 100644 --- a/inc/api/class-package-api.php +++ b/inc/api/class-package-api.php @@ -14,16 +14,51 @@ 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 { - protected $http_response_code = 200; - protected $api_key_id; - protected $api_access; - + /** + * Is doing API request + * + * @var bool|null + */ protected static $doing_api_request = null; + /** + * Instance + * + * @var Package_API|null + */ protected static $instance; + /** + * Config + * + * @var array|null + */ protected static $config; + /** + * HTTP response code + * + * @var int|null + */ + protected $http_response_code = 200; + /** + * API key ID + * + * @var string|null + */ + protected $api_key_id; + /** + * API access + * + * @var array|null + */ + protected $api_access; + public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -63,6 +98,11 @@ class Package_API { */ $result = apply_filters( 'upserv_package_browse', $result, $query ); + /** + * Fired after the `browse` Package API action. + * + * @param array $result the result of the action + */ do_action( 'upserv_did_browse_package', $result ); if ( empty( $result ) ) { @@ -103,6 +143,11 @@ class Package_API { */ $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 + */ do_action( 'upserv_did_read_package', $result ); if ( ! $result ) { @@ -162,6 +207,11 @@ class Package_API { '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 + */ do_action( 'upserv_did_edit_package', $result ); } @@ -214,6 +264,11 @@ class Package_API { '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 + */ do_action( 'upserv_did_add_package', $result ); } @@ -221,6 +276,12 @@ class Package_API { } 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 + */ do_action( 'upserv_pre_delete_package', $package_id, $type ); $result = upserv_delete_package( $package_id ); @@ -235,6 +296,13 @@ class Package_API { $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 + */ do_action( 'upserv_did_delete_package', $result, $package_id, $type ); } else { $this->http_response_code = 404; @@ -261,6 +329,11 @@ class Package_API { } 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 + */ do_action( 'upserv_did_download_package', $package_id ); exit; @@ -317,6 +390,11 @@ class Package_API { ); if ( $result ) { + /** + * Fired after the `signed_url` Package API action. + * + * @param array $result the result of the action + */ do_action( 'upserv_did_signed_url_package', $result ); } else { $this->http_response_code = 404; @@ -584,6 +662,12 @@ class Package_API { self::$config = $config; } + /** + * Filter the configuration of the Package API. + * + * @param array $config The configuration of the Package API + * @return array The filtered configuration + */ return apply_filters( 'upserv_package_api_config', self::$config ); } @@ -710,6 +794,14 @@ class Package_API { 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 + */ do_action( 'upserv_saved_remote_package_to_local', true, $type, $package_id ); return $result; @@ -781,6 +873,13 @@ class Package_API { } 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 + */ $public_api = apply_filters( 'upserv_package_public_api_actions', array( 'download' ) @@ -819,6 +918,14 @@ class Package_API { } 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 + */ $authorized = apply_filters( 'upserv_package_api_request_authorized', ( @@ -836,6 +943,12 @@ class Package_API { ); 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 + */ do_action( 'upserv_package_api_request', $method, $payload ); if ( method_exists( $this, $method ) ) { From 475b89c0f31ec5f8e2be968ef2f62cc97899c73c Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:46:51 +0800 Subject: [PATCH 26/49] Full documentation WIP --- docs/licenses.md | 17 +- inc/api/class-license-api.php | 112 ++++--- inc/api/class-package-api.php | 318 +++++++++++++++++- inc/api/class-update-api.php | 233 ++++++++++++- inc/api/class-webhook-api.php | 300 ++++++++++++++++- inc/cli/class-cli.php | 107 +++++- inc/manager/class-api-manager.php | 132 ++++++++ inc/manager/class-cloud-storage-manager.php | 342 ++++++++++++++++++++ inc/manager/class-data-manager.php | 210 +++++++++++- inc/manager/class-license-manager.php | 284 +++++++++++++++- 10 files changed, 1995 insertions(+), 60 deletions(-) diff --git a/docs/licenses.md b/docs/licenses.md index 8c254de..5de29ff 100644 --- a/docs/licenses.md +++ b/docs/licenses.md @@ -66,7 +66,8 @@ UpdatePulse Server provides an API and offers a series of functions, actions and * [upserv\_license\_api\_request\_authorized](#upserv_license_api_request_authorized) * [upserv\_license\_bypass\_signature](#upserv_license_bypass_signature) * [upserv\_api\_license\_actions](#upserv_api_license_actions) - * [upserv\_api\_license\_actions](#upserv_api_license_actions-1) + * [upserv\_license\_update\_server\_prepare\_license\_for\_output](#upserv_license_update_server_prepare_license_for_output) + * [upserv\_schedule\_license\_frequency](#upserv_schedule_license_frequency) ___ ## The License Query @@ -1622,7 +1623,7 @@ Filter the License API actions available for API access control. > (array) the API actions ___ -### upserv_api_license_actions +### upserv_license_update_server_prepare_license_for_output ```php apply_filters( 'upserv_license_update_server_prepare_license_for_output', array $output, object $license ); @@ -1639,3 +1640,15 @@ Filter the license data to send to the remote client. > (array) the original license object ___ +### upserv_schedule_license_frequency + +```php +apply_filters( 'upserv_schedule_license_frequency', string $frequency ); +``` +**Description** +Filter the frequency at which the license maintenance task runs. + +**Parameters** +`$frequency` +> (string) the frequency at which the license maintenance task runs. Default is `hourly` (see [WP_Cron](https://developer.wordpress.org/reference/classes/wp_cron/) for more information on the available frequencies) +___ \ No newline at end of file diff --git a/inc/api/class-license-api.php b/inc/api/class-license-api.php index c864f9d..1c84584 100644 --- a/inc/api/class-license-api.php +++ b/inc/api/class-license-api.php @@ -22,18 +22,21 @@ class License_API { * Is doing API request * * @var boolean|null + * @since 1.0.0 */ protected static $doing_api_request = null; /** * Instance * * @var License_API|null + * @since 1.0.0 */ protected static $instance; /** * Config * * @var array|null + * @since 1.0.0 */ protected static $config; @@ -41,34 +44,37 @@ class License_API { * License server * * @var License_Server + * @since 1.0.0 */ protected $license_server; /** * HTTP response code * * @var int|null + * @since 1.0.0 */ protected $http_response_code = null; /** * 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 * - * @since 1.0.0 - * * @param boolean $init_hooks * @param boolean $local_request + * @since 1.0.0 */ public function __construct( $init_hooks = false, $local_request = true ) { @@ -107,10 +113,9 @@ class License_API { /** * Browse licenses * - * @since 1.0.0 - * * @param string $query * @return object Result of the browse operation + * @since 1.0.0 */ public function browse( $query ) { $payload = json_decode( wp_unslash( $query ), true ); @@ -203,10 +208,9 @@ class License_API { /** * Read license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the read operation + * @since 1.0.0 */ public function read( $license_data ) { $result = wp_cache_get( @@ -255,10 +259,9 @@ class License_API { /** * Edit license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the edit operation + * @since 1.0.0 */ public function edit( $license_data ) { @@ -321,10 +324,9 @@ class License_API { /** * Add license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the add operation + * @since 1.0.0 */ public function add( $license_data ) { @@ -367,10 +369,9 @@ class License_API { /** * Delete license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the delete operation + * @since 1.0.0 */ public function delete( $license_data ) { $result = $this->license_server->delete_license( $license_data ); @@ -404,16 +405,16 @@ class License_API { /** * Check license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the check operation + * @since 1.0.0 */ public function check( $license_data ) { /** * Filter the license data payload before checking a license. * * @param array $license_data The license data payload. + * @since 1.0.0 */ $license_data = apply_filters( 'upserv_check_license_dirty_payload', $license_data ); $license = $this->license_server->read_license( $license_data ); @@ -433,6 +434,7 @@ class License_API { * * @param object|null $license The license object or null if not found. * @param array $license_data The license data payload. + * @since 1.0.0 */ $result = apply_filters( 'upserv_check_license_result', $license, $license_data ); @@ -440,6 +442,7 @@ class License_API { * Fired after checking a license. * * @param mixed $raw_result The raw result of the license check. + * @since 1.0.0 */ do_action( 'upserv_did_check_license', $raw_result ); @@ -453,16 +456,16 @@ class License_API { /** * Activate license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the activate operation + * @since 1.0.0 */ public function activate( $license_data ) { /** * Filter the license data payload before activating a license. * * @param array $license_data The license data payload. + * @since 1.0.0 */ $license_data = apply_filters( 'upserv_activate_license_dirty_payload', $license_data ); @@ -476,6 +479,7 @@ class License_API { * Fired before activating a license. * * @param object $license The license object. + * @since 1.0.0 */ do_action( 'upserv_pre_activate_license', $license ); @@ -494,6 +498,7 @@ class License_API { * @param object $result The result of the license activation. * @param array $license_data The license data payload. * @param object $license The license object. + * @since 1.0.0 */ $result = apply_filters( 'upserv_activate_license_result', $result, $license_data, $license ); @@ -502,6 +507,7 @@ class License_API { * * @param mixed $raw_result The raw result of the license activation. * @param array $license_data The license data payload. + * @since 1.0.0 */ do_action( 'upserv_did_activate_license', $raw_result, $license_data ); @@ -511,16 +517,16 @@ class License_API { /** * Deactivate license * - * @since 1.0.0 - * * @param array $license_data * @return object Result of the deactivate operation + * @since 1.0.0 */ public function deactivate( $license_data ) { /** * Filter the license data payload before deactivating a license. * * @param array $license_data The license data payload. + * @since 1.0.0 */ $license_data = apply_filters( 'upserv_deactivate_license_dirty_payload', $license_data ); @@ -534,6 +540,7 @@ class License_API { * Fired before deactivating a license. * * @param object $license The license object. + * @since 1.0.0 */ do_action( 'upserv_pre_deactivate_license', $license ); @@ -552,6 +559,7 @@ class License_API { * @param object $result The result of the license deactivation. * @param array $license_data The license data payload. * @param object $license The license object. + * @since 1.0.0 */ $result = apply_filters( 'upserv_deactivate_license_result', $result, $license_data, $license ); @@ -560,6 +568,7 @@ class License_API { * * @param mixed $raw_result The raw result of the license deactivation. * @param array $license_data The license data payload. + * @since 1.0.0 */ do_action( 'upserv_did_deactivate_license', $raw_result, $license_data ); @@ -604,10 +613,9 @@ class License_API { /** * Query vars filter * - * @since 1.0.0 - * * @param array $query_vars * @return array The filtered query vars + * @since 1.0.0 */ public function query_vars( $query_vars ) { $query_vars = array_merge( @@ -631,10 +639,9 @@ class License_API { /** * Filter update request params * - * @since 1.0.0 - * * @param array $params * @return array The filtered params + * @since 1.0.0 */ public function upserv_handle_update_request_params( $params ) { global $wp; @@ -653,10 +660,9 @@ class License_API { /** * Filter license actions * - * @since 1.0.0 - * * @param array $actions * @return array The filtered actions + * @since 1.0.0 */ public function upserv_api_license_actions( $actions ) { $actions['browse'] = __( 'Browse multiple license records', 'updatepulse-server' ); @@ -671,10 +677,9 @@ class License_API { /** * Filter webhook events * - * @since 1.0.0 - * * @param array $webhook_events * @return array The filtered webhook events + * @since 1.0.0 */ public function upserv_api_webhook_events( $webhook_events ) { @@ -701,11 +706,10 @@ class License_API { /** * License action * - * @since 1.0.0 - * * @param object $result * @param array $payload * @param object $original + * @since 1.0.0 */ public function upserv_did_license_action( $result, $payload, $original = null ) { $format = ''; @@ -781,13 +785,12 @@ class License_API { /** * Webhook fire filter * - * @since 1.0.0 - * * @param boolean $fire * @param array $payload * @param string $url * @param array $info * @return boolean The filtered fire value + * @since 1.0.0 */ public function upserv_webhook_fire( $fire, $payload, $url, $info ) { @@ -854,13 +857,12 @@ class License_API { /** * Fetch nonce filter * - * @since 1.0.0 - * * @param string $nonce * @param string $true_nonce * @param int $expiry * @param array $data * @return string|null The filterd nonce or null if invalid + * @since 1.0.0 */ public function upserv_fetch_nonce_private( $nonce, $true_nonce, $expiry, $data ) { $config = self::get_config(); @@ -904,10 +906,9 @@ class License_API { /** * Nonce API payload filter * - * @since 1.0.0 - * * @param array $payload * @return array The filtered payload + * @since 1.0.0 */ public function upserv_nonce_api_payload( $payload ) { global $wp; @@ -957,9 +958,8 @@ class License_API { /** * Is doing API request * - * @since 1.0.0 - * * @return boolean True if doing API request, false otherwise + * @since 1.0.0 */ public static function is_doing_api_request() { @@ -973,9 +973,8 @@ class License_API { /** * Get config * - * @since 1.0.0 - * * @return array The config + * @since 1.0.0 */ public static function get_config() { @@ -992,6 +991,7 @@ class License_API { * Filter the License API configuration. * * @param array $config The License API configuration. + * @since 1.0.0 */ return apply_filters( 'upserv_license_api_config', self::$config ); } @@ -999,9 +999,8 @@ class License_API { /** * Get instance * - * @since 1.0.0 - * * @return License_API The instance + * @since 1.0.0 */ public static function get_instance() { @@ -1015,10 +1014,9 @@ class License_API { /** * Is package require license * - * @since 1.0.0 - * * @param int $package_id * @return boolean True if package requires license, false otherwise + * @since 1.0.0 */ public static function is_package_require_license( $package_id ) { $require_license = wp_cache_get( 'upserv_package_require_license_' . $package_id, 'updatepulse-server', false, $found ); @@ -1044,9 +1042,9 @@ class License_API { /** * Sanitize license result * - * @since 1.0.0 * @param object $result - by reference * @return void + * @since 1.0.0 */ protected function sanitize_license_result( &$result ) { $num_allowed_domains = ( @@ -1077,6 +1075,7 @@ class License_API { * @param string $message * @param array $data * @return array The response + * @since 1.0.0 */ protected function prepare_error_response( $code, $message, $data = array() ) { return array( @@ -1091,6 +1090,7 @@ class License_API { * * @param array $license_data - by reference * @return void + * @since 1.0.0 */ protected function normalize_allowed_domains( &$license_data ) { @@ -1104,6 +1104,7 @@ class License_API { * * @param array $license_data * @return string|false The first domain found or false if not found + * @since 1.0.0 */ protected function extract_domain_from_license_data( $license_data ) { @@ -1125,6 +1126,7 @@ class License_API { * @param string $request_slug * @param string $domain * @return boolean True if valid, false otherwise + * @since 1.0.0 */ protected function is_valid_license_for_state_transition( $license, $request_slug, $domain ) { return ( @@ -1141,6 +1143,7 @@ class License_API { * @param object $license * @param string $domain * @return array|null The result or null if not found + * @since 1.0.0 */ protected function handle_license_activation( $license, $domain ) { $domain_count = count( $license->allowed_domains ) + 1; @@ -1167,6 +1170,7 @@ class License_API { * * @param object $license * @return array The response + * @since 1.0.0 */ protected function prepare_illegal_status_response( $license ) { $response = array( @@ -1189,6 +1193,7 @@ class License_API { * * @param object $license * @return array The response + * @since 1.0.0 */ protected function prepare_max_domains_response( $license ) { return array( @@ -1205,6 +1210,7 @@ class License_API { * * @param string $domain * @return array The response + * @since 1.0.0 */ protected function prepare_already_activated_response( $domain ) { return array( @@ -1222,6 +1228,7 @@ class License_API { * @param object $license * @param string $domain * @return array|null The result or null if not found + * @since 1.0.0 */ protected function process_license_activation( $license, $domain ) { $data = isset( $license->data ) ? $license->data : array(); @@ -1232,6 +1239,7 @@ class License_API { * * @param int $timestamp The timestamp for the next allowed deactivation. * @param object $license The license object. + * @since 1.0.0 */ $data['next_deactivate'] = apply_filters( 'upserv_activate_license_next_deactivate', time(), $license ); } @@ -1249,6 +1257,7 @@ class License_API { * Filter the payload for license activation. * * @param array $payload The license activation payload. + * @since 1.0.0 */ apply_filters( 'upserv_activate_license_payload', $payload ) ); @@ -1278,6 +1287,7 @@ class License_API { * @param object $license * @param string $domain * @return array|null The result or null if not found + * @since 1.0.0 */ protected function handle_license_deactivation( $license, $domain ) { @@ -1306,6 +1316,7 @@ class License_API { * * @param string $domain * @return array The response + * @since 1.0.0 */ protected function prepare_already_deactivated_response( $domain ) { return array( @@ -1322,6 +1333,7 @@ class License_API { * * @param object $license * @return array The response + * @since 1.0.0 */ protected function prepare_too_early_deactivation_response( $license ) { return array( @@ -1339,6 +1351,7 @@ class License_API { * @param object $license * @param string $domain * @return array|null The result or null if not found + * @since 1.0.0 */ protected function process_license_deactivation( $license, $domain ) { $data = isset( $license->data ) ? $license->data : array(); @@ -1347,6 +1360,7 @@ class License_API { * * @param int $timestamp The timestamp for the next allowed deactivation. * @param object $license The license object. + * @since 1.0.0 */ $data['next_deactivate'] = apply_filters( 'upserv_deactivate_license_next_deactivate', @@ -1368,6 +1382,7 @@ class License_API { * Filter the payload for license deactivation. * * @param array $payload The license deactivation payload. + * @since 1.0.0 */ apply_filters( 'upserv_activate_license_payload', $payload ) ); @@ -1397,6 +1412,7 @@ class License_API { * @param array $license * @param array $license_data * @return array The response + * @since 1.0.0 */ protected function handle_invalid_license( $license, $license_data ) { @@ -1431,6 +1447,7 @@ class License_API { * @param string $action * @param array $payload * @return boolean True if authorized, false otherwise + * @since 1.0.0 */ protected function authorize_private( $action, $payload ) { $token = false; @@ -1497,12 +1514,14 @@ class License_API { * * @param string $method * @return boolean True if public, false otherwise + * @since 1.0.0 */ protected function is_api_public( $method ) { /** * Filter the list of public License API actions. * * @param array $public_api_actions List of public License API actions. + * @since 1.0.0 */ $public_api = apply_filters( 'upserv_license_public_api_actions', @@ -1563,6 +1582,7 @@ class License_API { * @param bool $authorized Whether the License API request is authorized. * @param string $method The method of the request. * @param array $payload The payload of the request. + * @since 1.0.0 */ $authorized = apply_filters( 'upserv_license_api_request_authorized', @@ -1583,6 +1603,7 @@ class License_API { * * @param string $method The License API action. * @param array $payload The payload of the request. + * @since 1.0.0 */ do_action( 'upserv_license_api_request', $method, $payload ); @@ -1621,9 +1642,8 @@ class License_API { /** * Authorize IP * - * @since 1.0.0 - * * @return boolean True if authorized, false otherwise + * @since 1.0.0 */ protected function authorize_ip() { $result = false; @@ -1649,12 +1669,14 @@ class License_API { /** * Init server * + * @since 1.0.0 */ protected function init_server() { /** * Filter the License Server instance. * * @param License_Server $license_server The license server instance. + * @since 1.0.0 */ $this->license_server = apply_filters( 'upserv_license_server', new License_Server() ); } diff --git a/inc/api/class-package-api.php b/inc/api/class-package-api.php index a32caff..5d2ed3c 100644 --- a/inc/api/class-package-api.php +++ b/inc/api/class-package-api.php @@ -25,18 +25,21 @@ 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; @@ -44,21 +47,30 @@ class Package_API { * 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 ) { @@ -83,6 +95,15 @@ class Package_API { // 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 ); @@ -95,6 +116,7 @@ class 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 ); @@ -102,6 +124,7 @@ class Package_API { * 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 ); @@ -120,6 +143,16 @@ class Package_API { 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 ); @@ -140,6 +173,7 @@ class Package_API { * @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 ); @@ -147,6 +181,7 @@ class Package_API { * 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 ); @@ -161,6 +196,16 @@ class Package_API { 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(); @@ -185,6 +230,7 @@ class Package_API { * @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 ); @@ -211,6 +257,7 @@ class Package_API { * 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 ); } @@ -218,6 +265,16 @@ class Package_API { 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(); @@ -242,6 +299,7 @@ class Package_API { * @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 ); @@ -268,6 +326,7 @@ class Package_API { * 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 ); } @@ -275,12 +334,23 @@ class Package_API { 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 ); @@ -292,6 +362,7 @@ class Package_API { * @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 ); @@ -302,6 +373,7 @@ class Package_API { * @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 { @@ -315,6 +387,16 @@ class Package_API { 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 ); @@ -333,12 +415,23 @@ class Package_API { * 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 ); @@ -349,7 +442,8 @@ class Package_API { * @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 ) { @@ -371,6 +465,7 @@ class Package_API { * @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', @@ -394,6 +489,7 @@ class Package_API { * 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 { @@ -409,6 +505,13 @@ class Package_API { // 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)/(.+)/*?$', @@ -422,6 +525,13 @@ class Package_API { ); } + /** + * Parse API requests + * + * Handle incoming API requests to the Package API endpoints. + * + * @since 1.0.0 + */ public function parse_request() { global $wp; @@ -432,6 +542,15 @@ class Package_API { } } + /** + * 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, @@ -450,6 +569,16 @@ class Package_API { 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 ) { @@ -468,6 +597,15 @@ class Package_API { 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, @@ -476,6 +614,16 @@ class Package_API { ); } + /** + * 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, @@ -494,6 +642,14 @@ class Package_API { } } + /** + * 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', @@ -505,6 +661,15 @@ class Package_API { 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' ); @@ -516,6 +681,15 @@ class Package_API { 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'] ) ) { @@ -527,6 +701,18 @@ class Package_API { 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; @@ -555,6 +741,18 @@ class Package_API { 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; @@ -592,6 +790,15 @@ class Package_API { 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; @@ -635,12 +842,30 @@ class Package_API { 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 ) { @@ -650,6 +875,14 @@ class 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 ) { @@ -667,10 +900,19 @@ class 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 ) { @@ -684,6 +926,14 @@ class Package_API { * 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; @@ -700,6 +950,17 @@ class Package_API { 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, @@ -801,12 +1062,23 @@ class Package_API { * @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' ) ) ); @@ -822,6 +1094,14 @@ class Package_API { 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' ) ) ); @@ -838,6 +1118,15 @@ class Package_API { 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; @@ -872,6 +1161,15 @@ class Package_API { 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, @@ -879,6 +1177,7 @@ class Package_API { * * @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', @@ -889,6 +1188,13 @@ class Package_API { 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; @@ -925,6 +1231,7 @@ class Package_API { * @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', @@ -948,6 +1255,7 @@ class Package_API { * * @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 ); @@ -990,6 +1298,14 @@ class Package_API { 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(); diff --git a/inc/api/class-update-api.php b/inc/api/class-update-api.php index 0d5e598..ef3cc1a 100644 --- a/inc/api/class-update-api.php +++ b/inc/api/class-update-api.php @@ -10,13 +10,42 @@ 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 ) { @@ -38,6 +67,13 @@ class Update_API { // 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/*$', @@ -46,6 +82,13 @@ class Update_API { ); } + /** + * Parse API requests + * + * Handle incoming API requests to the Update API endpoints. + * + * @since 1.0.0 + */ public function parse_request() { global $wp; @@ -54,6 +97,15 @@ class Update_API { } } + /** + * 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, @@ -69,10 +121,29 @@ class Update_API { 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 ) { @@ -80,6 +151,16 @@ class Update_API { } } + /** + * 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 ) { @@ -87,6 +168,18 @@ class Update_API { } } + /** + * 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'] ); @@ -94,6 +187,13 @@ class Update_API { 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'], @@ -109,6 +209,18 @@ class Update_API { 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'] ); @@ -116,6 +228,13 @@ class Update_API { 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'], @@ -133,6 +252,14 @@ class Update_API { // 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 ) { @@ -142,6 +269,14 @@ class 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 ) { @@ -151,6 +286,16 @@ class Update_API { 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 ); @@ -163,6 +308,17 @@ class Update_API { 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; @@ -215,6 +371,14 @@ class Update_API { * 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 ); @@ -238,6 +402,14 @@ class Update_API { 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'], @@ -252,6 +424,18 @@ class Update_API { $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, @@ -263,6 +447,13 @@ class Update_API { ); } + /** + * Handle API requests + * + * Process and respond to Update API requests. + * + * @since 1.0.0 + */ protected function handle_api_request() { global $wp; @@ -288,6 +479,13 @@ class Update_API { 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'] ); @@ -303,10 +501,25 @@ class Update_API { ); } + /** + * 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; @@ -341,13 +554,31 @@ class Update_API { '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 ); - $args = apply_filters( + /** + * 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/' ), diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 0a0b8b4..96cc337 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -13,14 +13,48 @@ use Anyape\UpdatePulse\Server\Manager\Data_Manager; use Anyape\UpdatePulse\Server\Scheduler\Scheduler; use Anyape\Utils\Utils; +/** + *Webhook API class + * + *@since 1.0.0 + */ class Webhook_API { + /** + *Is doing API request + * + *@var bool|null + */ protected static $doing_api_request = null; + + /** + *Instance + * + *@var Webhook_API|null + */ protected static $instance; + /** + *Webhooks configuration + * + *@var array + */ protected $webhooks; + + /** + *HTTP response code + * + *@var int + */ protected $http_response_code = 200; + /** + *Constructor + * + *@since 1.0.0 + * + *@param boolean $init_hooks Whether to initialize hooks + */ public function __construct( $init_hooks = false ) { $this->webhooks = upserv_get_option( 'api/webhooks', array() ); $vcs_configs = upserv_get_option( 'vcs', array() ); @@ -51,11 +85,16 @@ class Webhook_API { } /******************************************************************* - * Public methods + *Public methods *******************************************************************/ // WordPress hooks --------------------------------------------- + /** + *Add API endpoints + * + *Register the rewrite rules for the Webhook API endpoints. + */ public function add_endpoints() { add_rewrite_rule( '^updatepulse-server-webhook$', 'index.php?__upserv_webhook=1&', 'top' ); add_rewrite_rule( @@ -65,6 +104,11 @@ class Webhook_API { ); } + /** + *Parse API requests + * + *Handle incoming API requests to the Webhook API endpoints. + */ public function parse_request() { global $wp; @@ -75,6 +119,14 @@ class Webhook_API { } } + /** + *Register query variables + * + *Add custom query variables used by the Webhook API. + * + *@param array $query_vars Existing query variables. + *@return array Modified query variables. + */ public function query_vars( $query_vars ) { $query_vars = array_merge( $query_vars, @@ -88,6 +140,11 @@ class Webhook_API { return $query_vars; } + /** + *Handle invalid webhook requests + * + *Display error page for unauthorized webhook requests. + */ public function upserv_webhook_invalid_request() { $protocol = empty( $_SERVER['SERVER_PROTOCOL'] ) ? 'HTTP/1.1' : sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); @@ -105,12 +162,32 @@ class Webhook_API { exit( -1 ); } + /** + *Process webhook requests + * + *Determine whether to process webhook requests based on branch matching. + * + *@param bool $process Current process status. + *@param array $payload Request payload. + *@param string $slug Package slug. + *@param string $type Package type. + *@param bool $package_exists Whether package already exists. + *@param array $vcs_config Version control system configuration. + *@return bool Whether to process the webhook request. + */ public function upserv_webhook_process_request( $process, $payload, $slug, $type, $package_exists, $vcs_config ) { return $this->get_payload_vcs_branch( $payload ) === $vcs_config['branch']; } // Misc. ------------------------------------------------------- + /** + *Check if currently processing an API request + * + *Determine whether the current request is a Webhook API request. + * + *@return bool Whether the current request is a Webhook API request. + */ public static function is_doing_api_request() { if ( null === self::$doing_api_request ) { @@ -120,6 +197,13 @@ class Webhook_API { return self::$doing_api_request; } + /** + *Get Webhook API instance + * + *Retrieve or create the Webhook API singleton instance. + * + *@return Webhook_API The Webhook API instance. + */ public static function get_instance() { if ( ! self::$instance ) { @@ -129,6 +213,16 @@ class Webhook_API { return self::$instance; } + /** + *Schedule webhook + * + *Schedule a webhook to be fired based on an event. + * + *@param array $payload Webhook payload data. + *@param string $event_type Event type identifier. + *@param bool $instant Whether to fire webhook immediately. + *@return void|WP_Error WP_Error on failure. + */ public function schedule_webhook( $payload, $event_type, $instant = false ) { if ( empty( $this->webhooks ) ) { @@ -169,12 +263,29 @@ class Webhook_API { } } + /** + * Filter whether to fire the webhook event. + * + * @param bool $fire Whether to fire the event. + * @param array $payload The payload of the event. + * @param string $url The target url of the event. + * @param array $webhook_setting The settings of the webhook. + * @return bool + */ if ( apply_filters( 'upserv_webhook_fire', $fire, $payload, $info['url'], $info ) ) { $body = wp_json_encode( $payload, Utils::JSON_OPTIONS ); $hook = 'upserv_webhook'; $params = array( $info['url'], $info['secret'], $body, current_action() ); if ( ! Scheduler::get_instance()->has_scheduled_action( $hook, $params ) ) { + /** + * Filter whether to send the webhook notification immediately. + * + * @param bool $instant Whether to send the notification immediately. + * @param array $payload The payload of the event. + * @param string $event_type The type of event. + * @return bool + */ $instant = apply_filters( 'upserv_schedule_webhook_is_instant', $instant, @@ -194,6 +305,17 @@ class Webhook_API { } } + /** + *Fire webhook + * + *Send an HTTP request to the webhook endpoint. + * + *@param string $url Webhook endpoint URL. + *@param string $secret Secret key for signature. + *@param string $body Request body. + *@param string $action Current action. + *@return array|WP_Error HTTP response or WP_Error on failure. + */ public function fire_webhook( $url, $secret, $body, $action ) { return wp_remote_post( $url, @@ -210,9 +332,14 @@ class Webhook_API { } /******************************************************************* - * Protected methods + *Protected methods *******************************************************************/ + /** + *Handle remote test + * + *Process and respond to webhook test requests. + */ protected function handle_remote_test() { if ( empty( $_SERVER['HTTP_X_UPDATEPULSE_SIGNATURE_256'] ) ) { @@ -266,6 +393,11 @@ class Webhook_API { wp_send_json( $valid, $valid ? 200 : 403, Utils::JSON_OPTIONS ); } + /** + *Handle API request + * + *Process webhook API requests and return appropriate responses. + */ protected function handle_api_request() { global $wp; @@ -281,6 +413,11 @@ class Webhook_API { $vcs_key = hash( 'sha256', trailingslashit( $url ) . '|' . $branch ); $vcs_config = isset( $vcs_configs[ $vcs_key ] ) ? $vcs_configs[ $vcs_key ] : false; + /** + * Fired before handling a webhook request; fired whether it will be processed or not. + * + * @param array $config The configuration used to handle webhook requests. + */ do_action( 'upserv_webhook_before_handling_request', $vcs_config ); if ( $vcs_config && $this->validate_request( $vcs_config ) ) { @@ -293,6 +430,16 @@ class Webhook_API { $delay = $vcs_config['check_delay']; $dir = Data_Manager::get_data_dir( 'packages' ); $package_exists = null; + /** + * Filter whether the package exists on the file system before processing the Webhook. + * + * @param bool|null $package_exists Whether the package exists on the file system; return `null` to leave the decision to the default behavior. + * @param array $payload The payload of the request. + * @param string $slug The slug of the package. + * @param string $type The type of the package. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return bool|null + */ $package_exists = apply_filters( 'upserv_webhook_package_exists', $package_exists, @@ -307,6 +454,17 @@ class Webhook_API { $package_exists = file_exists( $package_path ); } + /** + * Filter whether to process the Webhook request. + * + * @param bool $process Whether to process the Webhook request. + * @param array $payload The payload of the request. + * @param string $slug The slug of the package. + * @param string $type The type of the package. + * @param bool $package_exists Whether the package exists on the file system. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return bool + */ $process = apply_filters( 'upserv_webhook_process_request', true, @@ -318,6 +476,15 @@ class Webhook_API { ); if ( $process ) { + /** + * Fired before processing a webhook request. + * + * @param array $payload The data sent by the Version Control System. + * @param string $slug The slug of the package triggering the webhook. + * @param string $type The type of the package triggering the webhook. + * @param bool $package_exists Whether the package exists on the file system. + * @param array $vcs_config The configuration used to handle webhook requests. + */ do_action( 'upserv_webhook_before_processing_request', $payload, @@ -339,15 +506,38 @@ class Webhook_API { if ( ! $scheduled_action ) { Scheduler::get_instance()->unschedule_all_actions( $hook ); + /** + * Fired after a remote check schedule event has been unscheduled for a package. + * + * @param string $package_slug The slug of the package for which a remote check event has been unscheduled. + * @param string $scheduled_hook The remote check event hook that has been unscheduled. + */ do_action( 'upserv_cleared_check_remote_schedule', $slug, $hook ); } + /** + * Filter the delay time for remote package checks. + * + * @param int $delay The delay time in minutes. + * @param string $slug The slug of the package. + * @return int + */ $delay = apply_filters( 'upserv_check_remote_delay', $delay, $slug ); $timestamp = ( $delay ) ? time() + ( abs( intval( $delay ) ) * MINUTE_IN_SECONDS ) : time(); $result = Scheduler::get_instance()->schedule_single_action( $timestamp, $hook, $params ); + /** + * Fired after scheduling a remote check event. + * + * @param bool $result Whether the event was successfully scheduled. + * @param string $slug The slug of the package triggering the webhook. + * @param int $timestamp The timestamp when the event is scheduled to run. + * @param bool $is_cron Whether the event is a cron job. + * @param string $hook The hook name for the scheduled event. + * @param array $params The parameters passed to the scheduled event. + */ do_action( 'upserv_scheduled_check_remote_event', $result, @@ -381,6 +571,12 @@ class Webhook_API { } } else { Scheduler::get_instance()->unschedule_all_actions( $hook ); + /** + * Fired after a remote check schedule event has been unscheduled for a package. + * + * @param string $package_slug The slug of the package for which a remote check event has been unscheduled. + * @param string $scheduled_hook The remote check event hook that has been unscheduled. + */ do_action( 'upserv_cleared_check_remote_schedule', $slug, $hook ); $result = upserv_download_remote_package( $slug, $type ); @@ -402,6 +598,15 @@ class Webhook_API { } } + /** + * Fired after processing a webhook request. + * + * @param array $payload The data sent by the Version Control System. + * @param string $slug The slug of the package triggering the webhook. + * @param string $type The type of the package triggering the webhook. + * @param bool $package_exists Whether the package exists on the file system. + * @param array $vcs_config The configuration used to handle webhook requests. + */ do_action( 'upserv_webhook_after_processing_request', $payload, @@ -418,6 +623,11 @@ class Webhook_API { 'message' => __( 'Invalid request signature', 'updatepulse-server' ), ); + /** + * Fired when a webhook request is invalid. + * + * @param array $config The configuration used to handle webhook requests. + */ do_action( 'upserv_webhook_invalid_request', $vcs_config ); } @@ -425,19 +635,58 @@ class Webhook_API { $response['time_elapsed'] = Utils::get_time_elapsed(); } + /** + * Filter the response data to send to the Version Control System after handling the webhook request. + * + * @param array $response The response data to send to the Version Control System. + * @param int $http_response_code The HTTP response code. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return array + */ $response = apply_filters( 'upserv_webhook_response', $response, $this->http_response_code, $vcs_config ); + /** + * Fired after handling a webhook request; fired whether it was processed or not. + * + * @param array $config The configuration used to handle webhook requests. + * @param array $response The response data that will be sent to the Version Control System. + */ do_action( 'upserv_webhook_after_handling_request', $vcs_config, $response ); wp_send_json( $response, $this->http_response_code, Utils::JSON_OPTIONS ); } + /** + *Validate webhook request + * + *Verify webhook request signature against stored secrets. + * + *@param array $vcs_config Version control system configuration. + *@return bool Whether the request signature is valid. + */ protected function validate_request( $vcs_config ) { $valid = false; $sign = false; $secret = $vcs_config && isset( $vcs_config['webhook_secret'] ) ? $vcs_config['webhook_secret'] : false; + + /** + * Filter the webhook secret used for request validation. + * + * @param string|bool $secret The secret key for webhook validation. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return string|bool + */ $secret = apply_filters( 'upserv_webhook_secret', $secret, $vcs_config ); if ( ! $vcs_config || ! $secret ) { + /** + * Filter whether the webhook request is valid after validation. + * + * @param bool $valid Whether the request signature is valid. + * @param string|bool $sign The signature from the request. + * @param string $secret The secret key for webhook validation. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return bool + */ return apply_filters( 'upserv_webhook_validate_request', $valid, $sign, '', $vcs_config ); } @@ -451,6 +700,14 @@ class Webhook_API { $sign = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_HUB_SIGNATURE'] ) ); } + /** + * Filter the signature from the webhook request. + * + * @param string|bool $sign The signature from the request. + * @param string $secret The secret key for webhook validation. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return string|bool + */ $sign = apply_filters( 'upserv_webhook_signature', $sign, $secret, $vcs_config ); if ( $sign ) { @@ -462,9 +719,25 @@ class Webhook_API { } } + /** + * Filter whether the webhook request is valid after validation. + * + * @param bool $valid Whether the request signature is valid. + * @param string|bool $sign The signature from the request. + * @param string $secret The secret key for webhook validation. + * @param array $vcs_config The configuration used to handle webhook requests. + * @return bool + */ return apply_filters( 'upserv_webhook_validate_request', $valid, $sign, $secret, $vcs_config ); } + /** + *Get webhook payload + * + *Extract and decode the payload from the webhook request. + * + *@return array Decoded webhook payload. + */ protected function get_payload() { $payload = @file_get_contents( 'php://input' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.NoSilencedErrors.Discouraged $decoded = json_decode( $payload, true ); @@ -482,6 +755,14 @@ class Webhook_API { return ! is_array( $decoded ) ? array( 'decoded' => $decoded ) : $decoded; } + /** + *Get VCS URL from payload + * + *Extract the version control system URL from webhook payload. + * + *@param array $payload Webhook payload. + *@return string|false VCS URL or false if not found. + */ protected function get_payload_vcs_url( $payload ) { $url = false; @@ -500,6 +781,13 @@ class Webhook_API { $url = $payload['repository']['links']['html']['href']; } + /** + * Filter the Version Control System URL extracted from the webhook payload. + * + * @param string|bool $url The URL of the Version Control System. + * @param array $payload The webhook payload data. + * @return string|bool + */ $url = apply_filters( 'upserv_webhook_vcs_url', $url, $payload ); $parsed_url = wp_parse_url( $url ); @@ -520,6 +808,14 @@ class Webhook_API { return trailingslashit( $url ); } + /** + *Get VCS branch from payload + * + *Extract the branch information from webhook payload. + * + *@param array $payload Webhook payload. + *@return string|false Branch name or false if not found. + */ protected function get_payload_vcs_branch( $payload ) { $branch = false; diff --git a/inc/cli/class-cli.php b/inc/cli/class-cli.php index 2e18a88..b88b12b 100644 --- a/inc/cli/class-cli.php +++ b/inc/cli/class-cli.php @@ -11,11 +11,34 @@ use WP_CLI; use WP_Error; use Anyape\UpdatePulse\Server\Nonce\Nonce; +/** + * CLI commands for UpdatePulse Server. + * + * @since 1.0.0 + */ class CLI extends WP_CLI_Command { + /** + * Error code for when a resource is not found. + * + * @var int + * @since 1.0.0 + */ protected const RESOURCE_NOT_FOUND = 3; - protected const DEFAULT_ERROR = 1; - protected const LOG_METHODS = array( + /** + * Default error code for general errors. + * + * @var int + * @since 1.0.0 + */ + protected const DEFAULT_ERROR = 1; + /** + * Available log methods for WP_CLI. + * + * @var array + * @since 1.0.0 + */ + protected const LOG_METHODS = array( 'line', 'log', 'success', @@ -25,7 +48,13 @@ class CLI extends WP_CLI_Command { 'halt', 'error_multi_line', ); - protected const PACKAGE_TYPES = array( + /** + * Available package types supported by the plugin. + * + * @var array + * @since 1.0.0 + */ + protected const PACKAGE_TYPES = array( 'plugin', 'theme', 'generic', @@ -41,6 +70,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse cleanup_cache + * + * @since 1.0.0 */ public function cleanup_cache() { $this->cleanup( 'cache' ); @@ -52,6 +83,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse cleanup_logs + * + * @since 1.0.0 */ public function cleanup_logs() { $this->cleanup( 'logs' ); @@ -63,6 +96,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse cleanup_tmp + * + * @since 1.0.0 */ public function cleanup_tmp() { $this->cleanup( 'tmp' ); @@ -74,6 +109,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse cleanup-all + * + * @since 1.0.0 */ public function cleanup_all() { $this->cleanup( 'cache' ); @@ -95,6 +132,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse check_remote_package_update my-package plugin + * + * @since 1.0.0 */ public function check_remote_package_update( $args, $assoc_args ) { $slug = $args[0]; @@ -134,6 +173,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse download_remote_package my-package plugin --vcs_url='https://vcs-url.tld/identifier/' --branch='main' + * + * @since 1.0.0 */ public function download_remote_package( $args, $assoc_args ) { @@ -175,6 +216,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse delete_package my-package + * + * @since 1.0.0 */ public function delete_package( $args, $assoc_args ) { $slug = $args[0]; @@ -196,6 +239,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse get_package_info my-package + * + * @since 1.0.0 */ public function get_package_info( $args, $assoc_args ) { $slug = $args[0]; @@ -228,6 +273,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse create_nonce --true_nonce=true --expiry_length=30 --data='{}' --return=nonce_only --store=true + * + * @since 1.0.0 */ public function create_nonce( $args, $assoc_args ) { $assoc_args = wp_parse_args( @@ -298,6 +345,7 @@ class CLI extends WP_CLI_Command { * * wp updatepulse build_nonce_api_signature --api_key_id='UPDATEPULSE_L_api_key_name' --timestamp=1704067200 --api_key=da9d20647163a1f3c04844387f91e2c3 --payload='{"key": "value"}' * + * @since 1.0.0 */ public function build_nonce_api_signature( $args, $assoc_args ) { $assoc_args = wp_parse_args( @@ -333,6 +381,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse clear_nonces + * + * @since 1.0.0 */ public function clear_nonces() { $result = upserv_clear_nonces(); @@ -353,6 +403,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse get_nonce_expiry + * + * @since 1.0.0 */ public function get_nonce_expiry( $args, $assoc_args ) { $nonce = $args[0]; @@ -374,6 +426,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse get_nonce_data + * + * @since 1.0.0 */ public function get_nonce_data( $args, $assoc_args ) { $nonce = $args[0]; @@ -395,6 +449,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse delete_nonce + * + * @since 1.0.0 */ public function delete_nonce( $args, $assoc_args ) { $nonce = $args[0]; @@ -416,6 +472,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse browse_licenses + * + * @since 1.0.0 */ public function browse_licenses( $args, $assoc_args ) { $result = upserv_browse_licenses( $args[0] ); @@ -436,6 +494,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse read_license + * + * @since 1.0.0 */ public function read_license( $args, $assoc_args ) { $license_data = array(); @@ -464,6 +524,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse add_license + * + * @since 1.0.0 */ public function add_license( $args, $assoc_args ) { $payload = json_decode( $args[0], true ); @@ -488,6 +550,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse edit_license + * + * @since 1.0.0 */ public function edit_license( $args, $assoc_args ) { $payload = json_decode( $args[0], true ); @@ -512,6 +576,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse delete_license + * + * @since 1.0.0 */ public function delete_license( $args, $assoc_args ) { $license_data = array(); @@ -539,6 +605,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse check_license + * + * @since 1.0.0 */ public function check_license( $args, $assoc_args ) { $license_data = array(); @@ -576,6 +644,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse activate_license + * + * @since 1.0.0 */ public function activate_license( $args, $assoc_args ) { $license_data = array( @@ -610,6 +680,8 @@ class CLI extends WP_CLI_Command { * ## EXAMPLES * * wp updatepulse deactivate_license + * + * @since 1.0.0 */ public function deactivate_license( $args, $assoc_args ) { $license_data = array( @@ -631,6 +703,13 @@ class CLI extends WP_CLI_Command { * Protected methods *******************************************************************/ + /** + * Cleans up a specific folder in the plugin directory. + * + * @param string $method The folder to clean up ('cache', 'logs', or 'tmp'). + * @return void + * @since 1.0.0 + */ protected function cleanup( $method ) { $method = 'upserv_force_cleanup_' . $method; $result = $method(); @@ -640,6 +719,17 @@ class CLI extends WP_CLI_Command { $this->process_result( $result, $success_message, $error_message ); } + /** + * Processes the result of a command and outputs a message based on success or failure. + * + * @param mixed $result The result to evaluate. + * @param mixed $success_message Message to display on success. + * @param string $error_message Message to display on error. + * @param int $error_code Error code to return on failure. Default: 1. + * @param string $error_level WP_CLI error level to use. Default: 'warning'. + * @return void + * @since 1.0.0 + */ protected function process_result( $result, $success_message, $error_message, $error_code = 1, $error_level = 'warning' ) { if ( $result instanceof WP_Error ) { @@ -680,6 +770,17 @@ class CLI extends WP_CLI_Command { } } + /** + * Outputs a message to the CLI. + * + * Handles both string and array message formats. If an array is provided, + * it should contain 'level' and 'output' keys. The level determines which + * WP_CLI output method to use. + * + * @param string|array $message The message to output. + * @return void + * @since 1.0.0 + */ protected function output( $message ) { if ( is_string( $message ) ) { diff --git a/inc/manager/class-api-manager.php b/inc/manager/class-api-manager.php index eb1bab8..4d3ef6e 100644 --- a/inc/manager/class-api-manager.php +++ b/inc/manager/class-api-manager.php @@ -8,8 +8,19 @@ if ( ! defined( 'ABSPATH' ) ) { use stdClass; +/** + * API Manager class + * + * @since 1.0.0 + */ class API_Manager { + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -28,6 +39,15 @@ class API_Manager { // WordPress hooks --------------------------------------------- + /** + * Register admin styles + * + * Add custom styles used by the API admin interface. + * + * @param array $styles Existing admin styles. + * @return array Modified admin styles. + * @since 1.0.0 + */ public function upserv_admin_styles( $styles ) { $styles['api'] = array( 'path' => UPSERV_PLUGIN_PATH . 'css/admin/api' . upserv_assets_suffix() . '.css', @@ -37,6 +57,15 @@ class API_Manager { return $styles; } + /** + * Register admin scripts + * + * Add custom scripts used by the API admin interface. + * + * @param array $scripts Existing admin scripts. + * @return array Modified admin scripts. + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -93,6 +122,13 @@ class API_Manager { return $scripts; } + /** + * Register admin menu + * + * Add the API settings page to the admin menu. + * + * @since 1.0.0 + */ public function admin_menu() { $function = array( $this, 'plugin_page' ); $page_title = __( 'UpdatePulse Server - API & Webhooks', 'updatepulse-server' ); @@ -102,6 +138,15 @@ class API_Manager { add_submenu_page( 'upserv-page', $page_title, $menu_title, 'manage_options', $menu_slug, $function ); } + /** + * Register admin tab links + * + * Add API tab to the admin navigation. + * + * @param array $links Existing tab links. + * @return array Modified tab links. + * @since 1.0.0 + */ public function upserv_admin_tab_links( $links ) { $links['api'] = array( admin_url( 'admin.php?page=upserv-page-api' ), @@ -111,6 +156,16 @@ class API_Manager { return $links; } + /** + * Register admin tab states + * + * Set active state for API tab in admin navigation. + * + * @param array $states Existing tab states. + * @param string $page Current admin page. + * @return array Modified tab states. + * @since 1.0.0 + */ public function upserv_admin_tab_states( $states, $page ) { $states['api'] = 'upserv-page-api' === $page; @@ -119,6 +174,13 @@ class API_Manager { // Misc. ------------------------------------------------------- + /** + * Render plugin page + * + * Output the API settings admin interface. + * + * @since 1.0.0 + */ public function plugin_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -160,14 +222,35 @@ class API_Manager { 'plugin-api-page.php', array( 'options' => $options, + /** + * Filter the list of available License API actions. + * + * @param array $actions The list of available License API actions + * @return array The filtered list of actions + * @since 1.0.0 + */ 'license_api_actions' => apply_filters( 'upserv_api_license_actions', array() ), + /** + * Filter the list of available Package API actions. + * + * @param array $actions The list of available Package API actions + * @return array The filtered list of actions + * @since 1.0.0 + */ 'package_api_actions' => apply_filters( 'upserv_api_package_actions', array() ), + /** + * Filter the list of available webhook events. + * + * @param array $webhook_events The list of available webhook events + * @return array The filtered list of webhook events + * @since 1.0.0 + */ 'webhook_events' => apply_filters( 'upserv_api_webhook_events', array( @@ -189,6 +272,14 @@ class API_Manager { * Protected methods *******************************************************************/ + /** + * Handle plugin options + * + * Process and save API settings form submissions. + * + * @return string|array Success message or array of errors. + * @since 1.0.0 + */ protected function plugin_options_handler() { $errors = array(); $result = ''; @@ -355,6 +446,16 @@ class API_Manager { } } + /** + * Filter whether an API option should be updated. + * + * @param bool $condition Whether the condition for updating the option is met + * @param string $option_name The name of the option + * @param array $option_info Information about the option + * @param array $options All submitted options + * @return bool Whether the option should be updated + * @since 1.0.0 + */ $condition = apply_filters( 'upserv_api_option_update', $condition, @@ -364,6 +465,16 @@ class API_Manager { ); if ( $condition ) { + /** + * Filter the value of an API option before it is saved. + * + * @param mixed $value The value to save + * @param string $option_name The name of the option + * @param array $option_info Information about the option + * @param array $options All submitted options + * @return mixed The filtered value to save + * @since 1.0.0 + */ $to_save[ $option_info['path'] ] = apply_filters( 'upserv_api_option_save_value', $option_info['value'], @@ -395,12 +506,33 @@ class API_Manager { $result = $errors; } + /** + * Fired after API options have been updated. + * + * @param array $errors Array of errors that occurred during the update process + * @since 1.0.0 + */ do_action( 'upserv_api_options_updated', $errors ); return $result; } + /** + * Get submitted options + * + * Retrieve and sanitize form data from API settings form. + * + * @return array Sanitized form data. + * @since 1.0.0 + */ protected function get_submitted_options() { + /** + * Filter the submitted API configuration options. + * + * @param array $config The submitted API configuration options + * @return array The filtered configuration options + * @since 1.0.0 + */ return apply_filters( 'upserv_submitted_api_config', array( diff --git a/inc/manager/class-cloud-storage-manager.php b/inc/manager/class-cloud-storage-manager.php index 48ec15d..a94a0f8 100644 --- a/inc/manager/class-cloud-storage-manager.php +++ b/inc/manager/class-cloud-storage-manager.php @@ -18,18 +18,70 @@ use Anyape\UpdatePulse\Server\Server\Update\Package; use Anyape\UpdatePulse\Package_Parser\Parser; use Anyape\Utils\Utils; +/** + * Cloud Storage Manager class + * + * Handles integration with S3-compatible cloud storage for package management. + * + * @since 1.0.0 + */ class Cloud_Storage_Manager { + /** + * Instance of the Cloud Storage Manager + * + * @var Cloud_Storage_Manager|null + * @since 1.0.0 + */ protected static $instance; + /** + * Cloud storage configuration + * + * @var array|null + * @since 1.0.0 + */ protected static $config; + /** + * Cloud storage client instance + * + * @var PhpS3|null + * @since 1.0.0 + */ protected static $cloud_storage; + /** + * Virtual directory path in cloud storage + * + * @var string|null + * @since 1.0.0 + */ protected static $virtual_dir; + /** + * Hooks registered by the manager + * + * @var array + * @since 1.0.0 + */ protected static $hooks = array(); + /** + * Whether we're currently performing a redirect + * + * @var bool + * @since 1.0.0 + */ protected $doing_redirect = false; + /** + * Download URL lifetime in seconds + */ public const DOWNLOAD_URL_LIFETIME = MINUTE_IN_SECONDS; + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { $config = self::get_config(); @@ -57,6 +109,12 @@ class Cloud_Storage_Manager { } } + /** + * Initialize cloud storage manager + * + * @param array $config Cloud storage configuration + * @since 1.0.0 + */ protected function init_manager( $config ) { if ( ! self::$cloud_storage instanceof PhpS3 ) { @@ -70,10 +128,22 @@ class Cloud_Storage_Manager { self::$cloud_storage->setExceptions(); + /** + * Filter the virtual directory path used in cloud storage. + * + * @param string $virtual_dir The default virtual directory name + * @return string The filtered virtual directory name + * @since 1.0.0 + */ self::$virtual_dir = apply_filters( 'upserv_cloud_storage_virtual_dir', 'updatepulse-packages' ); } } + /** + * Add hooks for cloud storage functionality + * + * @since 1.0.0 + */ protected function add_hooks() { if ( ! empty( self::$hooks ) ) { @@ -136,6 +206,11 @@ class Cloud_Storage_Manager { } } + /** + * Remove hooks for cloud storage functionality + * + * @since 1.0.0 + */ protected function remove_hooks() { if ( empty( self::$hooks ) ) { @@ -159,6 +234,13 @@ class Cloud_Storage_Manager { self::$hooks = array(); } + /** + * Get cloud storage configuration + * + * @param boolean $force Whether to force reload the configuration + * @return array Cloud storage configuration + * @since 1.0.0 + */ public static function get_config( $force = false ) { if ( $force || ! self::$config ) { @@ -168,9 +250,22 @@ class Cloud_Storage_Manager { self::$config = $config; } + /** + * Filter the configuration of the Cloud Storage Manager. + * + * @param array $config The configuration of the Cloud Storage Manager + * @return array The filtered configuration + * @since 1.0.0 + */ return apply_filters( 'upserv_cloud_storage_config', self::$config ); } + /** + * Get Cloud Storage Manager instance + * + * @return Cloud_Storage_Manager The singleton instance + * @since 1.0.0 + */ public static function get_instance() { if ( ! self::$instance ) { @@ -180,6 +275,13 @@ class Cloud_Storage_Manager { return self::$instance; } + /** + * Add cloud storage-specific scripts to admin + * + * @param array $scripts Existing registered scripts + * @return array Modified scripts array + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -196,6 +298,13 @@ class Cloud_Storage_Manager { return $scripts; } + /** + * Process submitted package configurations + * + * @param array $config Existing configuration + * @return array Modified configuration + * @since 1.0.0 + */ public function upserv_submitted_package_config( $config ) { $config = array_merge( $config, @@ -268,6 +377,16 @@ class Cloud_Storage_Manager { return $config; } + /** + * Validate package option updates + * + * @param boolean $condition Current validation condition + * @param string $option_name Option being updated + * @param array $option_info Option information + * @param array $options All options being processed + * @return boolean Whether option is valid + * @since 1.0.0 + */ public function upserv_package_option_update( $condition, $option_name, $option_info, $options ) { if ( 'use-cloud-storage' === $option_info['condition'] ) { @@ -300,6 +419,11 @@ class Cloud_Storage_Manager { return $condition; } + /** + * Render cloud storage options in template + * + * @since 1.0.0 + */ public function upserv_template_package_manager_option_before_miscellaneous() { $options = array( 'access_key' => upserv_get_option( 'cloud_storage/access_key' ), @@ -319,6 +443,13 @@ class Cloud_Storage_Manager { ); } + /** + * Add cloud storage paths to bulk delete + * + * @param array $package_paths Current package paths + * @return array Modified package paths + * @since 1.0.0 + */ public function upserv_delete_packages_bulk_paths( $package_paths ) { $config = self::get_config(); @@ -347,6 +478,15 @@ class Cloud_Storage_Manager { return $package_paths; } + /** + * Check if package exists in cloud storage + * + * @param boolean $package_exists Current existence state + * @param array $payload Request payload + * @param string $slug Package slug + * @return boolean|null Whether package exists in cloud storage + * @since 1.0.0 + */ public function upserv_webhook_package_exists( $package_exists, $payload, $slug ) { if ( null !== $package_exists ) { @@ -383,6 +523,15 @@ class Cloud_Storage_Manager { } } + /** + * Process package removal from cloud storage + * + * @param boolean $result Current removal result + * @param string $type Package type + * @param string $slug Package slug + * @return boolean Whether removal was successful + * @since 1.0.0 + */ public function upserv_remove_package_result( $result, $type, $slug ) { $config = self::get_config(); @@ -427,6 +576,14 @@ class Cloud_Storage_Manager { return $result; } + /** + * Modify admin template arguments + * + * @param array $args Current template arguments + * @param string $template_name Template being rendered + * @return array Modified template arguments + * @since 1.0.0 + */ public function upserv_get_admin_template_args( $args, $template_name ) { $template_names = array( 'plugin-packages-page.php', 'plugin-help-page.php', 'plugin-remote-sources-page.php' ); @@ -437,6 +594,13 @@ class Cloud_Storage_Manager { return $args; } + /** + * Test cloud storage connectivity + * + * AJAX handler for testing cloud storage configuration + * + * @since 1.0.0 + */ public function cloud_storage_test() { $result = array(); $nonce = sanitize_text_field( wp_unslash( filter_input( INPUT_POST, 'nonce' ) ) ); @@ -527,6 +691,13 @@ class Cloud_Storage_Manager { } } + /** + * Handle package options update + * + * Set up cloud storage after options are updated + * + * @since 1.0.0 + */ public function upserv_package_options_updated() { $config = self::get_config( true ); @@ -566,6 +737,15 @@ class Cloud_Storage_Manager { } } + /** + * Update local package metadata from cloud storage + * + * @param array|false $local_meta Current local metadata + * @param object $local_package Local package instance + * @param string $slug Package slug + * @return array|false Updated metadata or false + * @since 1.0.0 + */ public function upserv_check_remote_package_update_local_meta( $local_meta, $local_package, $slug ) { if ( ! $local_meta ) { @@ -606,6 +786,14 @@ class Cloud_Storage_Manager { return $local_meta; } + /** + * Handle saving remote package to cloud storage + * + * @param boolean $local_ready Whether local package is ready + * @param string $type Package type + * @param string $slug Package slug + * @since 1.0.0 + */ public function upserv_saved_remote_package_to_local( $local_ready, $type, $slug ) { $config = self::get_config(); $package_directory = Data_Manager::get_data_dir( 'packages' ); @@ -651,6 +839,14 @@ class Cloud_Storage_Manager { } } + /** + * Handle manual package upload to cloud storage + * + * @param boolean $result Upload result + * @param string $type Package type + * @param string $slug Package slug + * @since 1.0.0 + */ public function upserv_did_manual_upload_package( $result, $type, $slug ) { if ( ! $result ) { @@ -684,6 +880,16 @@ class Cloud_Storage_Manager { } } + /** + * Determine whether to save remote package locally + * + * @param boolean $save Current save decision + * @param string $slug Package slug + * @param string $filename Target filename + * @param boolean $check_remote Whether to check remote storage + * @return boolean Whether to save package locally + * @since 1.0.0 + */ public function upserv_save_remote_to_local( $save, $slug, $filename, $check_remote ) { $config = self::get_config(); @@ -718,6 +924,14 @@ class Cloud_Storage_Manager { return $save; } + /** + * Handle pre-download actions for packages + * + * @param string $archive_name Archive name + * @param string $archive_path Archive path + * @param array $package_slugs Package slugs to download + * @since 1.0.0 + */ public function upserv_before_packages_download( $archive_name, $archive_path, $package_slugs ) { if ( 1 === count( $package_slugs ) ) { @@ -753,6 +967,14 @@ class Cloud_Storage_Manager { } } + /** + * Handle pre-download repack actions for packages + * + * @param string $archive_name Archive name + * @param string $archive_path Archive path + * @param array $package_slugs Package slugs to download + * @since 1.0.0 + */ public function upserv_before_packages_download_repack( $archive_name, $archive_path, $package_slugs ) { if ( ! empty( $package_slugs ) ) { @@ -783,6 +1005,13 @@ class Cloud_Storage_Manager { } } + /** + * Handle post-download actions for packages + * + * @param string $archive_name Archive name + * @param string $archive_path Archive path + * @since 1.0.0 + */ public function upserv_after_packages_download( $archive_name, $archive_path ) { if ( is_file( $archive_path ) ) { @@ -790,6 +1019,13 @@ class Cloud_Storage_Manager { } } + /** + * Handle package API requests + * + * @param string $method API method being called + * @param array $payload Request payload + * @since 1.0.0 + */ public function upserv_package_api_request( $method, $payload ) { $config = self::get_config(); @@ -828,12 +1064,26 @@ class Cloud_Storage_Manager { ); $this->doing_redirect = wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + /** + * Fired after a package is downloaded. + * + * @param string $package_slug the slug of the downloaded package + * @since 1.0.0 + */ do_action( 'upserv_did_download_package', $package_id ); exit; } } + /** + * Fetch package from cloud storage when not in cache + * + * @param string $slug Package slug + * @param string $filename Target filename + * @param object $cache Cache instance + * @since 1.0.0 + */ public function upserv_find_package_no_cache( $slug, $filename, $cache ) { if ( is_file( $filename ) ) { @@ -887,12 +1137,29 @@ class Cloud_Storage_Manager { } } + /** + * Generate cache key for cloud storage metadata + * + * @param string $cache_key Current cache key + * @param string $slug Package slug + * @param string $filename Package filename + * @return string Modified cache key + * @since 1.0.0 + */ public function upserv_zip_metadata_parser_cache_key( $cache_key, $slug, $filename ) { $cloud_cache_key = self::build_cache_key( $slug, $filename ); return $cloud_cache_key ? $cloud_cache_key : $cache_key; } + /** + * Get package information from cloud storage + * + * @param array|false $package_info Current package information + * @param string $slug Package slug + * @return array|false Updated package information + * @since 1.0.0 + */ public function upserv_package_manager_get_package_info( $package_info, $slug ) { $cache = new Cache( Data_Manager::get_data_dir( 'cache' ) ); $config = self::get_config(); @@ -1018,6 +1285,14 @@ class Cloud_Storage_Manager { return $package_info; } + /** + * Get batch package information from cloud storage + * + * @param array $packages Current packages information + * @param string $search Search term + * @return array Updated packages information + * @since 1.0.0 + */ public function upserv_package_manager_get_batch_package_info( $packages, $search ) { $config = self::get_config(); $contents = wp_cache_get( 'upserv-getBucket', 'updatepulse-server' ); @@ -1054,6 +1329,14 @@ class Cloud_Storage_Manager { false === strpos( strtolower( $info['slug'] ) . '.zip', strtolower( $search ) ) ) ); + /** + * Filter 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 + */ $include = apply_filters( 'upserv_package_info_include', $include, $info ); if ( $include ) { @@ -1076,6 +1359,12 @@ class Cloud_Storage_Manager { return $packages; } + /** + * Handle package download action + * + * @param object $request Download request + * @since 1.0.0 + */ public function upserv_update_server_action_download( $request ) { $config = self::get_config(); $url = self::$cloud_storage->getAuthenticatedUrlV4( @@ -1087,10 +1376,24 @@ class Cloud_Storage_Manager { $this->doing_redirect = wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect } + /** + * Check if download request is already handled + * + * @return boolean Whether download is handled + * @since 1.0.0 + */ public function upserv_update_server_action_download_handled() { return $this->doing_redirect; } + /** + * Check if package is whitelisted in cloud storage + * + * @param boolean $whitelisted Current whitelist status + * @param string $package_slug Package slug + * @return boolean Updated whitelist status + * @since 1.0.0 + */ public function upserv_is_package_whitelisted( $whitelisted, $package_slug ) { $data = upserv_get_package_metadata( $package_slug, false ); @@ -1107,6 +1410,14 @@ class Cloud_Storage_Manager { return $whitelisted; } + /** + * Update package data when whitelisted + * + * @param array $data Package data + * @param string $slug Package slug + * @return array Updated package data + * @since 1.0.0 + */ public function upserv_whitelist_package_data( $data, $slug ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $data['whitelisted']['cloud'] = array( true, @@ -1116,6 +1427,14 @@ class Cloud_Storage_Manager { return $data; } + /** + * Update package data when unwhitelisted + * + * @param array $data Package data + * @param string $slug Package slug + * @return array Updated package data + * @since 1.0.0 + */ public function upserv_unwhitelist_package_data( $data, $slug ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $data['whitelisted']['cloud'] = array( false, @@ -1125,6 +1444,14 @@ class Cloud_Storage_Manager { return $data; } + /** + * Build cache key for cloud storage items + * + * @param string $slug Package slug + * @param string $filename Package filename + * @return string|false Cache key or false + * @since 1.0.0 + */ protected static function build_cache_key( $slug, $filename ) { $config = self::get_config(); $info = wp_cache_get( $slug . '-getObjectInfo', 'updatepulse-server' ); @@ -1147,6 +1474,13 @@ class Cloud_Storage_Manager { return $cache_key; } + /** + * Check if virtual folder exists in cloud storage + * + * @param string $name Folder name + * @return boolean Whether folder exists + * @since 1.0.0 + */ protected function virtual_folder_exists( $name ) { $config = self::get_config(); @@ -1156,6 +1490,14 @@ class Cloud_Storage_Manager { ); } + /** + * Create a virtual folder in cloud storage + * + * @param string $name Folder name + * @param string|null $storage_unit Storage unit name + * @return boolean Whether folder was created + * @since 1.0.0 + */ protected function create_virtual_folder( $name, $storage_unit = null ) { if ( ! $storage_unit ) { diff --git a/inc/manager/class-data-manager.php b/inc/manager/class-data-manager.php index b7c721a..d4454c3 100644 --- a/inc/manager/class-data-manager.php +++ b/inc/manager/class-data-manager.php @@ -12,21 +12,48 @@ use Anyape\UpdatePulse\Server\Scheduler\Scheduler; class Data_Manager { + /** + * Transient data directories + * + * List of directories that store temporary data. + * + * @var array + * @since 1.0.0 + */ public static $transient_data_dirs = array( 'cache', 'logs', 'tmp', ); - + /** + * Persistent data directories + * + * List of directories that store permanent data. + * + * @var array + * @since 1.0.0 + */ public static $persistent_data_dirs = array( 'packages', 'metadata', ); - + /** + * Transient data in database + * + * List of temporary data stored in the database. + * + * @var array + * @since 1.0.0 + */ public static $transient_data_db = array( 'update_from_remote_locks', ); - + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks. + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -40,6 +67,13 @@ class Data_Manager { // WordPress hooks --------------------------------------------- + /** + * Activate + * + * Actions to perform when the plugin is activated. + * + * @since 1.0.0 + */ public static function activate() { set_transient( 'upserv_flush', 1, 60 ); @@ -64,10 +98,24 @@ class Data_Manager { } } + /** + * Deactivate + * + * Actions to perform when the plugin is deactivated. + * + * @since 1.0.0 + */ public static function deactivate() { self::clear_schedules(); } + /** + * Initialize scheduler + * + * Register cleanup events and schedules. + * + * @since 1.0.0 + */ public function upserv_scheduler_init() { self::register_cleanup_events(); self::register_cleanup_schedules(); @@ -75,10 +123,25 @@ class Data_Manager { // Misc. ------------------------------------------------------- + /** + * Clear schedules + * + * Remove all scheduled cleanup events. + * + * @since 1.0.0 + */ public static function clear_schedules() { self::clear_cleanup_schedules(); } + /** + * Setup directories + * + * Create data directories if they don't exist. + * + * @return bool True if directories were created successfully, false otherwise. + * @since 1.0.0 + */ public static function maybe_setup_directories() { $root_dir = self::get_data_dir(); $result = true; @@ -100,6 +163,14 @@ class Data_Manager { return $result; } + /** + * Setup MU plugin + * + * Create or update the must-use plugin file. + * + * @return bool True if the MU plugin was setup successfully, false otherwise. + * @since 1.0.0 + */ public static function maybe_setup_mu_plugin() { global $wp_filesystem; @@ -125,6 +196,15 @@ class Data_Manager { return $result; } + /** + * Get data directory path + * + * Retrieve the path to a specific data directory. + * + * @param string $dir Directory name or 'root' for the base directory. + * @return string Path to the requested directory. + * @since 1.0.0 + */ public static function get_data_dir( $dir = 'root' ) { $data_dir = wp_cache_get( 'data_dir_' . $dir, 'updatepulse-server' ); @@ -156,6 +236,16 @@ class Data_Manager { return $data_dir; } + /** + * Check if directory is valid + * + * Determine whether a directory name is a valid data directory. + * + * @param string $dir The directory name to check. + * @param bool $require_persistent Whether the directory must be persistent. + * @return bool Whether the directory is valid. + * @since 1.0.0 + */ public static function is_valid_data_dir( $dir, $require_persistent = false ) { $is_valid = false; @@ -168,6 +258,16 @@ class Data_Manager { return $is_valid; } + /** + * Maybe cleanup data + * + * Clean up transient data if needed. + * + * @param string $type The type of data to clean up. + * @param bool $force Whether to force cleanup regardless of conditions. + * @return bool Whether cleanup was performed. + * @since 1.0.0 + */ public static function maybe_cleanup( $type, $force = false ) { if ( in_array( $type, self::$transient_data_db, true ) ) { @@ -191,6 +291,16 @@ class Data_Manager { * Protected methods *******************************************************************/ + /** + * Maybe cleanup data directory + * + * Clean up a data directory if it exceeds its size limit or if forced. + * + * @param string $type The directory to clean up. + * @param bool $force Whether to force cleanup regardless of conditions. + * @return bool Whether cleanup was performed. + * @since 1.0.0 + */ protected static function maybe_cleanup_data_dir( $type, $force ) { WP_Filesystem(); @@ -234,6 +344,15 @@ class Data_Manager { $result = $result && self::generate_restricted_htaccess( $directory ); } + /** + * Fired after a data directory cleanup operation. + * + * @param bool $result Whether the cleanup was successful + * @param string $type The type of data that was cleaned up + * @param int $total_size The total size of the data before cleanup + * @param bool $force Whether the cleanup was forced + * @since 1.0.0 + */ do_action( 'upserv_did_cleanup', $result, $type, $total_size, $force ); return $result; @@ -242,6 +361,14 @@ class Data_Manager { return false; } + /** + * Maybe cleanup update from remote locks + * + * Clean up expired remote update locks from the database. + * + * @return bool Whether cleanup was performed. + * @since 1.0.0 + */ protected static function maybe_cleanup_update_from_remote_locks() { $locks = get_option( 'upserv_update_from_remote_locks' ); @@ -258,6 +385,17 @@ class Data_Manager { } } + /** + * Create data directory + * + * Create a directory for storing plugin data. + * + * @param string $name The name of the directory to create. + * @param bool $include_htaccess Whether to create an .htaccess file. + * @param bool $is_root_dir Whether this is the root data directory. + * @return bool Whether the directory was created successfully. + * @since 1.0.0 + */ protected static function create_data_dir( $name, $include_htaccess = true, $is_root_dir = false ) { $wp_upload_dir = wp_upload_dir(); $root_dir = trailingslashit( $wp_upload_dir['basedir'] . '/updatepulse-server' ); @@ -271,6 +409,15 @@ class Data_Manager { return $result; } + /** + * Generate restricted htaccess + * + * Create an .htaccess file that prevents direct access to files. + * + * @param string $directory The directory path where to create the .htaccess file. + * @return bool Whether the .htaccess file was created successfully. + * @since 1.0.0 + */ protected static function generate_restricted_htaccess( $directory ) { WP_Filesystem(); @@ -288,6 +435,13 @@ class Data_Manager { return $wp_filesystem->put_contents( $htaccess, $contents, 0644 ); } + /** + * Clear cleanup schedules + * + * Unschedule all cleanup events. + * + * @since 1.0.0 + */ protected static function clear_cleanup_schedules() { if ( upserv_is_doing_update_api_request() ) { @@ -304,10 +458,26 @@ class Data_Manager { } Scheduler::get_instance()->unschedule_all_actions( 'upserv_cleanup', $params ); + + /** + * Fired after a cleanup schedule has been cleared. + * + * @param string $type The type of data for which the schedule was cleared + * @param array $params The parameters that were used for the schedule + * @since 1.0.0 + */ do_action( 'upserv_cleared_cleanup_schedule', $type, $params ); } } + /** + * Register cleanup schedules + * + * Register action hooks for cleanup events. + * + * @return bool Whether the schedules were registered successfully. + * @since 1.0.0 + */ protected static function register_cleanup_schedules() { if ( upserv_is_doing_update_api_request() ) { @@ -326,10 +496,25 @@ class Data_Manager { $hook = array( __NAMESPACE__ . '\\Data_Manager', 'maybe_cleanup' ); add_action( 'upserv_cleanup', $hook, 10, 2 ); + + /** + * Fired after a cleanup schedule has been registered. + * + * @param string $type The type of data for which the schedule was registered + * @param array $params The parameters that are used for the schedule + * @since 1.0.0 + */ do_action( 'upserv_registered_cleanup_schedule', $type, $params ); } } + /** + * Register cleanup events + * + * Schedule recurring cleanup events. + * + * @since 1.0.0 + */ protected static function register_cleanup_events() { $cleanable_datatypes = array_merge( self::$transient_data_dirs, self::$transient_data_db ); @@ -342,6 +527,14 @@ class Data_Manager { } if ( ! Scheduler::get_instance()->has_scheduled_action( $hook, $params ) ) { + /** + * Filter the cleanup schedule frequency. + * + * @param string $frequency The frequency of the cleanup schedule (default 'hourly') + * @param string $type The type of data to clean up + * @return string The filtered frequency + * @since 1.0.0 + */ $frequency = apply_filters( 'upserv_schedule_cleanup_frequency', 'hourly', $type ); $schedules = wp_get_schedules(); $timestamp = time(); @@ -352,6 +545,17 @@ class Data_Manager { $params ); + /** + * Fired after a cleanup event has been scheduled. + * + * @param bool $result Whether the scheduling was successful + * @param string $type The type of data for which the event was scheduled + * @param int $timestamp The timestamp at which the event will first run + * @param string $frequency The frequency of the scheduled event + * @param string $hook The hook that will be triggered + * @param array $params The parameters that will be passed to the hook + * @since 1.0.0 + */ do_action( 'upserv_scheduled_cleanup_event', $result, diff --git a/inc/manager/class-license-manager.php b/inc/manager/class-license-manager.php index e2992f8..be347ad 100644 --- a/inc/manager/class-license-manager.php +++ b/inc/manager/class-license-manager.php @@ -13,13 +13,48 @@ use Anyape\UpdatePulse\Server\Table\Licenses_Table; use Anyape\UpdatePulse\Server\Server\License\License_Server; use Anyape\UpdatePulse\Server\Scheduler\Scheduler; +/** + * License Manager class + * + * @since 1.0.0 + */ class License_Manager { + /** + * Licenses table + * + * @var Licenses_Table|null + * @since 1.0.0 + */ protected $licenses_table; + /** + * Message to display + * + * @var string + * @since 1.0.0 + */ protected $message = ''; - protected $errors = array(); + /** + * Error messages + * + * @var array + * @since 1.0.0 + */ + protected $errors = array(); + /** + * License server instance + * + * @var License_Server|null + * @since 1.0.0 + */ protected $license_server; + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -53,6 +88,13 @@ class License_Manager { // WordPress hooks --------------------------------------------- + /** + * Activate license system + * + * Creates necessary database tables and sets up license expiration schedule. + * + * @since 1.0.0 + */ public static function activate() { $result = self::maybe_create_or_upgrade_db(); @@ -62,22 +104,50 @@ class License_Manager { die( $error_message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - $manager = new self(); + $manager = new self(); + /** + * Filter the frequency at which the license maintenance task runs. + * + * @param string $frequency The WordPress schedule frequency (hourly, daily, etc.) + */ $frequency = apply_filters( 'upserv_schedule_license_frequency', 'hourly' ); $manager->register_license_schedules( $frequency ); } + /** + * Deactivate license system + * + * Removes scheduled license expiration tasks. + * + * @since 1.0.0 + */ public static function deactivate() { Scheduler::get_instance()->unschedule_all_actions( 'upserv_expire_licenses' ); + + /** + * Fired after the license maintenance event has been unscheduled. + */ do_action( 'upserv_cleared_license_schedule' ); } + /** + * Initialize scheduler + * + * Sets up recurring schedule for license expiration checks. + * + * @since 1.0.0 + */ public function upserv_scheduler_init() { $hook = 'upserv_expire_licenses'; if ( ! Scheduler::get_instance()->has_scheduled_action( $hook ) ) { - $frequency = apply_filters( 'upserv_schedule_license_frequency', 'daily' ); + /** + * Filter the frequency at which the license maintenance task runs. + * + * @param string $frequency The WordPress schedule frequency (daily, etc.) + */ + $frequency = apply_filters( 'upserv_schedule_license_frequency', 'hourly' ); $schedules = wp_get_schedules(); $d = new DateTime( 'now', new DateTimeZone( wp_timezone_string() ) ); @@ -90,12 +160,27 @@ class License_Manager { $hook ); + /** + * Fired after the license maintenance event has been scheduled. + * + * @param bool $result Whether the event was scheduled successfully + * @param int $timestamp Timestamp for when to run the event the first time + * @param string $frequency Frequency at which the event would be ran + * @param string $hook Event hook to fire when the event is ran + */ do_action( 'upserv_scheduled_license_event', $result, $timestamp, $frequency, $hook ); } $this->register_license_schedules(); } + /** + * Initialize admin area + * + * Sets up license table and processes form submissions. + * + * @since 1.0.0 + */ public function admin_init() { if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) { @@ -188,6 +273,15 @@ class License_Manager { } } + /** + * Register admin styles + * + * Adds license-specific styles to the admin area. + * + * @param array $styles Existing admin styles + * @return array Modified admin styles + * @since 1.0.0 + */ public function upserv_admin_styles( $styles ) { $styles['license'] = array( 'path' => UPSERV_PLUGIN_PATH . 'css/admin/license' . upserv_assets_suffix() . '.css', @@ -199,6 +293,15 @@ class License_Manager { return $styles; } + /** + * Register admin scripts + * + * Adds license-specific scripts to the admin area. + * + * @param array $scripts Existing admin scripts + * @return array Modified admin scripts + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -221,6 +324,12 @@ class License_Manager { 'params' => array( 'cm_settings' => wp_enqueue_code_editor( array( 'type' => 'text/json' ) ), ), + /** + * Filter the internationalization strings passed to the frontend scripts. + * + * @param array $l10n The internationalization strings passed to the frontend scripts + * @param string $handle The handle of the script + */ 'l10n' => apply_filters( 'upserv_scripts_l10n', $l10n, 'license' ), ); @@ -229,6 +338,13 @@ class License_Manager { return $scripts; } + /** + * Add page options + * + * Adds screen options for the licenses page. + * + * @since 1.0.0 + */ public function add_page_options() { $option = 'per_page'; $args = array( @@ -240,22 +356,61 @@ class License_Manager { add_screen_option( $option, $args ); } + /** + * Save page options + * + * Handles saving of screen options. + * + * @param mixed $status Status of the option + * @param string $option Option name + * @param mixed $value Option value + * @return mixed Filtered option value + * @since 1.0.0 + */ public function set_page_options( $status, $option, $value ) { return $value; } + /** + * Add package table columns + * + * Adds license columns to the packages table. + * + * @param array $columns Existing table columns + * @return array Modified table columns + * @since 1.0.0 + */ public function upserv_packages_table_columns( $columns ) { $columns['col_use_license'] = __( 'License status', 'updatepulse-server' ); return $columns; } + /** + * Add sortable columns + * + * Adds sortable license columns to the packages table. + * + * @param array $columns Existing sortable columns + * @return array Modified sortable columns + * @since 1.0.0 + */ public function upserv_packages_table_sortable_columns( $columns ) { $columns['col_use_license'] = __( 'License status', 'updatepulse-server' ); return $columns; } + /** + * Display package table cell content + * + * Populates license status cells in the packages table. + * + * @param string $column_name Name of the column + * @param array $record Record data + * @param string $record_key Record identifier + * @since 1.0.0 + */ public function upserv_packages_table_cell( $column_name, $record, $record_key ) { $use_license = upserv_is_package_require_license( $record_key ); @@ -264,6 +419,13 @@ class License_Manager { } } + /** + * Add admin menu + * + * Registers the licenses submenu page. + * + * @since 1.0.0 + */ public function admin_menu() { $function = array( $this, 'plugin_page' ); $page_title = __( 'UpdatePulse Server - Licenses', 'updatepulse-server' ); @@ -275,6 +437,15 @@ class License_Manager { add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $function ); } + /** + * Add admin tab links + * + * Adds licenses tab to the admin navigation. + * + * @param array $links Existing tab links + * @return array Modified tab links + * @since 1.0.0 + */ public function upserv_admin_tab_links( $links ) { $links['licenses'] = array( admin_url( 'admin.php?page=upserv-page-licenses' ), @@ -284,6 +455,16 @@ class License_Manager { return $links; } + /** + * Manage admin tab states + * + * Updates active state for the licenses tab. + * + * @param array $states Current tab states + * @param string $page Current page + * @return array Modified tab states + * @since 1.0.0 + */ public function upserv_admin_tab_states( $states, $page ) { $states['licenses'] = 'upserv-page-licenses' === $page; @@ -292,17 +473,44 @@ class License_Manager { // Misc. ------------------------------------------------------- + /** + * Expire licenses + * + * Changes status of expired licenses. + * + * @since 1.0.0 + */ public function expire_licenses() { $this->license_server->switch_expired_licenses_status(); } + /** + * Register license schedules + * + * Sets up hooks for scheduled license tasks. + * + * @since 1.0.0 + */ public function register_license_schedules() { $scheduled_hook = array( $this, 'expire_licenses' ); add_action( 'upserv_expire_licenses', $scheduled_hook, 10, 2 ); + + /** + * Fired after the license maintenance action has been registered. + * + * @param string $scheduled_hook The license event hook that has been registered + */ do_action( 'upserv_registered_license_schedule', $scheduled_hook ); } + /** + * Display plugin page + * + * Renders the licenses management page. + * + * @since 1.0.0 + */ public function plugin_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -349,6 +557,14 @@ class License_Manager { * Protected methods *******************************************************************/ + /** + * Create or upgrade database + * + * Sets up and updates the licenses database table. + * + * @return bool Whether database setup was successful + * @since 1.0.0 + */ protected static function maybe_create_or_upgrade_db() { global $wpdb; @@ -398,6 +614,14 @@ class License_Manager { return true; } + /** + * Handle plugin options + * + * Processes and saves plugin settings. + * + * @return string|bool Success message or false on failure + * @since 1.0.0 + */ protected function plugin_options_handler() { $errors = array(); $result = ''; @@ -456,7 +680,20 @@ class License_Manager { return $result; } + /** + * Get submitted options + * + * Retrieves and validates options from form submission. + * + * @return array Options array with validation parameters + * @since 1.0.0 + */ protected function get_submitted_options() { + /** + * Filter the submitted license configuration values before saving them. + * + * @param array $config The submitted license configuration values + */ return apply_filters( 'upserv_submitted_licenses_config', array( @@ -470,6 +707,15 @@ class License_Manager { ); } + /** + * Change license statuses in bulk + * + * Updates status for multiple licenses at once. + * + * @param string $status New status to apply + * @param array $license_data Licenses to update + * @since 1.0.0 + */ protected function change_license_statuses_bulk( $status, $license_data ) { if ( ! is_array( $license_data ) ) { @@ -524,6 +770,15 @@ class License_Manager { } } + /** + * Delete licenses in bulk + * + * Removes multiple licenses from the system. + * + * @param array $license_ids IDs of licenses to delete + * @return array IDs of deleted licenses + * @since 1.0.0 + */ protected function delete_license_bulk( $license_ids ) { if ( ! is_array( $license_ids ) ) { @@ -550,6 +805,14 @@ class License_Manager { return $license_ids; } + /** + * Update a license + * + * Updates an existing license with new data. + * + * @param string $license_data JSON encoded license data + * @since 1.0.0 + */ protected function update_license( $license_data ) { $payload = json_decode( $license_data, true ); $payload['data'] = json_decode( $payload['data'], true ); @@ -564,6 +827,14 @@ class License_Manager { } } + /** + * Create a new license + * + * Adds a new license to the system. + * + * @param string $license_data JSON encoded license data + * @since 1.0.0 + */ protected function create_license( $license_data ) { $payload = json_decode( $license_data, true ); $payload['data'] = json_decode( $payload['data'], true ); @@ -578,6 +849,13 @@ class License_Manager { } } + /** + * Delete all licenses + * + * Removes all licenses from the system. + * + * @since 1.0.0 + */ protected function delete_all_licenses() { $this->license_server->purge_licenses(); From 8020d655e3314464d3819c1493e7e116358f0ee0 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:11:16 +0800 Subject: [PATCH 27/49] See https://github.com/Anyape/updatepulse-server/issues/4 --- inc/api/class-webhook-api.php | 8 +- languages/updatepulse-server.pot | 360 +++++++++++++++---------------- 2 files changed, 184 insertions(+), 184 deletions(-) diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 96cc337..4b735f2 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -420,14 +420,14 @@ class Webhook_API { */ do_action( 'upserv_webhook_before_handling_request', $vcs_config ); - if ( $vcs_config && $this->validate_request( $vcs_config ) ) { + if ( $this->validate_request( $vcs_config ) ) { $slug = isset( $wp->query_vars['slug'] ) ? trim( rawurldecode( $wp->query_vars['slug'] ) ) : null; $type = isset( $wp->query_vars['type'] ) ? trim( rawurldecode( $wp->query_vars['type'] ) ) : null; - $delay = $vcs_config['check_delay']; + $delay = $vcs_config ? $vcs_config['check_delay'] : 0; $dir = Data_Manager::get_data_dir( 'packages' ); $package_exists = null; /** @@ -616,11 +616,11 @@ class Webhook_API { $vcs_config ); } - } elseif ( $vcs_config ) { + } else { $this->http_response_code = 403; $response = array( 'code' => 'unauthorized', - 'message' => __( 'Invalid request signature', 'updatepulse-server' ), + 'message' => __( 'Invalid request', 'updatepulse-server' ), ); /** diff --git a/languages/updatepulse-server.pot b/languages/updatepulse-server.pot index 8ea7eb5..55af109 100644 --- a/languages/updatepulse-server.pot +++ b/languages/updatepulse-server.pot @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: UpdatePulse Server\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-27 04:00+0000\n" +"POT-Creation-Date: 2025-03-20 00:10+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: \n" @@ -20,12 +20,12 @@ msgstr "" msgid " (discouraged)" msgstr "" -#: functions.php:627 +#: functions.php:1168 msgid "$url must be a valid url and $body must be a JSON string." msgstr "" #. %1$d is the number of events, %s is the type of events -#: inc/manager/class-api-manager.php:80 +#: inc/manager/class-api-manager.php:109 #, php-format msgid "%1$d %2$s events" msgstr "" @@ -37,12 +37,12 @@ msgid "%1$s: Method %2$s does not exist." msgstr "" #. %d is the number of actions -#: inc/manager/class-api-manager.php:69 +#: inc/manager/class-api-manager.php:98 msgid "%d actions" msgstr "" #. %d is the number of actions -#: inc/manager/class-api-manager.php:71 +#: inc/manager/class-api-manager.php:100 msgid "%d actions (all records)" msgstr "" @@ -55,20 +55,20 @@ msgid "(required)" msgstr "" #. %s is the type of event -#: inc/manager/class-api-manager.php:78 +#: inc/manager/class-api-manager.php:107 #, php-format msgid "1 %s event" msgstr "" -#: inc/manager/class-api-manager.php:66 +#: inc/manager/class-api-manager.php:95 msgid "1 action" msgstr "" -#: inc/manager/class-api-manager.php:67 +#: inc/manager/class-api-manager.php:96 msgid "1 action (all records)" msgstr "" -#: inc/api/class-webhook-api.php:99 inc/api/class-webhook-api.php:100 +#: inc/api/class-webhook-api.php:156 inc/api/class-webhook-api.php:157 msgid "401 Unauthorized" msgstr "" @@ -128,7 +128,7 @@ msgstr "" msgid "Action Scheduler" msgstr "" -#: inc/table/class-licenses-table.php:257 +#: inc/table/class-licenses-table.php:261 msgid "Activate" msgstr "" @@ -182,7 +182,7 @@ msgstr "" msgid "Add a Webhook" msgstr "" -#: inc/table/class-licenses-table.php:250 +#: inc/table/class-licenses-table.php:254 #: inc/templates/admin/plugin-licenses-page.php:15 msgid "Add License" msgstr "" @@ -231,16 +231,16 @@ msgid "Alexandre Froger" msgstr "" #. %s is the type of events -#: inc/manager/class-api-manager.php:76 +#: inc/manager/class-api-manager.php:105 #, php-format msgid "All %s events" msgstr "" -#: inc/manager/class-api-manager.php:72 +#: inc/manager/class-api-manager.php:101 msgid "All actions" msgstr "" -#: inc/manager/class-api-manager.php:73 +#: inc/manager/class-api-manager.php:102 msgid "All actions (all records)" msgstr "" @@ -250,7 +250,7 @@ msgid "" "superior to 5 characters." msgstr "" -#: inc/manager/class-api-manager.php:74 +#: inc/manager/class-api-manager.php:103 #: inc/templates/admin/plugin-api-page.php:153 msgid "All events" msgstr "" @@ -263,11 +263,11 @@ msgstr "" msgid "All packages will be permanently deleted." msgstr "" -#: inc/manager/class-license-manager.php:584 +#: inc/manager/class-license-manager.php:862 msgid "All the licenses have been deleted." msgstr "" -#: inc/manager/class-license-manager.php:211 +#: inc/manager/class-license-manager.php:314 msgid "All the records will be permanently deleted." msgstr "" @@ -276,12 +276,12 @@ msgid "Also grant access to affect other records (all records)" msgstr "" #. %s is operation slug -#: inc/api/class-license-api.php:489 +#: inc/api/class-license-api.php:725 #, php-format msgid "An error occured for License operation `%s` on UpdatePulse Server." msgstr "" -#: inc/manager/class-cloud-storage-manager.php:520 +#: inc/manager/class-cloud-storage-manager.php:684 msgid "" "An error occured when attempting to communicate with the Cloud Storage " "Service. Please check all the settings and try again." @@ -303,7 +303,7 @@ msgstr "" msgid "An unknown file upload error occurred." msgstr "" -#: inc/manager/class-api-manager.php:99 inc/manager/class-api-manager.php:108 +#: inc/manager/class-api-manager.php:135 inc/manager/class-api-manager.php:153 msgid "API & Webhooks" msgstr "" @@ -312,7 +312,7 @@ msgstr "" msgid "Archive max size (in MB)" msgstr "" -#: inc/manager/class-license-manager.php:216 +#: inc/manager/class-license-manager.php:319 msgid "Are you sure you want to delete this license?" msgstr "" @@ -322,9 +322,9 @@ msgstr "" #: inc/manager/class-package-manager.php:175 #: inc/manager/class-package-manager.php:182 -#: inc/manager/class-license-manager.php:214 -#: inc/manager/class-api-manager.php:52 inc/manager/class-api-manager.php:58 -#: inc/manager/class-api-manager.php:64 +#: inc/manager/class-license-manager.php:317 +#: inc/manager/class-api-manager.php:81 inc/manager/class-api-manager.php:87 +#: inc/manager/class-api-manager.php:93 msgid "Are you sure you want to do this?" msgstr "" @@ -337,11 +337,11 @@ msgid "" "UpdatePulse Server installation." msgstr "" -#: functions.php:29 inc/templates/admin/plugin-remote-sources-page.php:49 +#: functions.php:50 inc/templates/admin/plugin-remote-sources-page.php:49 msgid "Bitbucket" msgstr "" -#: inc/table/class-licenses-table.php:259 +#: inc/table/class-licenses-table.php:263 msgid "Block" msgstr "" @@ -349,7 +349,7 @@ msgstr "" msgid "Blocked" msgstr "" -#: inc/api/class-license-api.php:452 +#: inc/api/class-license-api.php:668 msgid "Browse multiple license records" msgstr "" @@ -421,31 +421,31 @@ msgstr "" msgid "Cloud Storage" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:216 +#: inc/manager/class-cloud-storage-manager.php:325 #: inc/templates/admin/cloud-storage-options.php:26 msgid "Cloud Storage Access Key" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:238 +#: inc/manager/class-cloud-storage-manager.php:347 #: inc/templates/admin/cloud-storage-options.php:48 msgid "Cloud Storage Endpoint" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:260 +#: inc/manager/class-cloud-storage-manager.php:369 #: inc/templates/admin/cloud-storage-options.php:77 msgid "Cloud Storage Region" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:227 +#: inc/manager/class-cloud-storage-manager.php:336 #: inc/templates/admin/cloud-storage-options.php:37 msgid "Cloud Storage Secret Key" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:491 +#: inc/manager/class-cloud-storage-manager.php:655 msgid "Cloud Storage Service was reached sucessfully." msgstr "" -#: inc/manager/class-cloud-storage-manager.php:249 +#: inc/manager/class-cloud-storage-manager.php:358 #: inc/templates/admin/cloud-storage-options.php:66 msgid "Cloud Storage Unit" msgstr "" @@ -468,7 +468,7 @@ msgstr "" msgid "Could not create temporary file." msgstr "" -#: inc/api/class-license-api.php:455 +#: inc/api/class-license-api.php:671 msgid "Create license records" msgstr "" @@ -496,7 +496,7 @@ msgstr "" msgid "Date Renewed" msgstr "" -#: inc/table/class-licenses-table.php:258 +#: inc/table/class-licenses-table.php:262 msgid "Deactivate" msgstr "" @@ -516,23 +516,23 @@ msgid "" "Repository for package updates when the Webhook has been called." msgstr "" -#: inc/table/class-licenses-table.php:261 +#: inc/table/class-licenses-table.php:265 msgid "Delete" msgstr "" -#: inc/api/class-package-api.php:381 +#: inc/api/class-package-api.php:678 msgid "Delete a package" msgstr "" -#: inc/table/class-licenses-table.php:250 +#: inc/table/class-licenses-table.php:254 msgid "Delete All Licenses" msgstr "" -#: inc/table/class-packages-table.php:243 +#: inc/table/class-packages-table.php:257 msgid "Delete All Packages" msgstr "" -#: inc/api/class-license-api.php:456 +#: inc/api/class-license-api.php:672 msgid "Delete license records" msgstr "" @@ -545,7 +545,7 @@ msgid "Details" msgstr "" #. %1$s is the path to the plugin's data directory -#: inc/manager/class-data-manager.php:139 +#: inc/manager/class-data-manager.php:219 #, php-format msgid "" "Directory %1$s is not a valid UpdatePulse Server data directory." @@ -555,12 +555,12 @@ msgstr "" msgid "Domains currently allowed to use this license." msgstr "" -#: inc/table/class-packages-table.php:260 +#: inc/table/class-packages-table.php:273 #: inc/templates/admin/packages-table-row.php:20 msgid "Download" msgstr "" -#: inc/table/class-packages-table.php:237 +#: inc/table/class-packages-table.php:251 msgid "" "Download: Archive max size exceeded - try to adjust it in the settings below." msgstr "" @@ -585,7 +585,7 @@ msgstr "" msgid "Edit License" msgstr "" -#: inc/manager/class-license-manager.php:465 +#: inc/manager/class-license-manager.php:702 #: inc/templates/admin/plugin-licenses-page.php:182 msgid "Enable Package Licenses" msgstr "" @@ -665,14 +665,14 @@ msgstr "" msgid "Error - Please check the provided Version Control System URL." msgstr "" -#: inc/manager/class-cloud-storage-manager.php:448 -#: inc/manager/class-cloud-storage-manager.php:460 +#: inc/manager/class-cloud-storage-manager.php:612 +#: inc/manager/class-cloud-storage-manager.php:624 #: inc/manager/class-remote-sources-manager.php:241 #: inc/manager/class-remote-sources-manager.php:253 msgid "Error - Received invalid data; please reload the page and try again." msgstr "" -#: inc/manager/class-cloud-storage-manager.php:486 +#: inc/manager/class-cloud-storage-manager.php:650 msgid "Error - Storage Unit not found" msgstr "" @@ -705,7 +705,7 @@ msgstr "" msgid "Expiration date of the license. Leave empty for no expiry." msgstr "" -#: inc/table/class-licenses-table.php:260 +#: inc/table/class-licenses-table.php:264 msgid "Expire" msgstr "" @@ -717,27 +717,27 @@ msgstr "" msgid "Extra Data" msgstr "" -#: inc/manager/class-license-manager.php:60 inc/nonce/class-nonce.php:38 +#: inc/manager/class-license-manager.php:102 inc/nonce/class-nonce.php:38 msgid "Failed to create the necessary database table(s)." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:400 +#: inc/api/class-webhook-api.php:595 #, php-format msgid "Failed to download package %s." msgstr "" -#: inc/manager/class-license-manager.php:576 +#: inc/manager/class-license-manager.php:847 msgid "Failed to insert the license record in the database." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:379 +#: inc/api/class-webhook-api.php:568 #, php-format msgid "Failed to sechedule download for package %s." msgstr "" -#: inc/manager/class-license-manager.php:562 +#: inc/manager/class-license-manager.php:825 msgid "Failed to update the license record in the database." msgstr "" @@ -745,6 +745,10 @@ msgstr "" msgid "Failed to write file to disk." msgstr "" +#: inc/table/class-packages-table.php:49 +msgid "File Modified " +msgstr "" + #: inc/table/class-packages-table.php:47 msgid "File Name" msgstr "" @@ -792,11 +796,11 @@ msgid "" "%s directory by entering the package slug." msgstr "" -#: inc/api/class-package-api.php:378 +#: inc/api/class-package-api.php:675 msgid "Get information about a single package" msgstr "" -#: inc/api/class-package-api.php:377 +#: inc/api/class-package-api.php:674 msgid "Get information about multiple packages" msgstr "" @@ -804,11 +808,11 @@ msgstr "" msgid "Get remote package" msgstr "" -#: inc/api/class-license-api.php:453 +#: inc/api/class-license-api.php:669 msgid "Get single license records" msgstr "" -#: functions.php:25 +#: functions.php:46 msgid "GitHub" msgstr "" @@ -816,7 +820,7 @@ msgstr "" msgid "Github" msgstr "" -#: functions.php:27 +#: functions.php:48 msgid "GitLab" msgstr "" @@ -866,13 +870,13 @@ msgstr "" msgid "ID" msgstr "" -#: inc/api/class-package-api.php:379 +#: inc/api/class-package-api.php:676 msgid "" "If a package does exist, update it by uploading a valid package file, or by " "downloading it if using a VCS" msgstr "" -#: inc/api/class-package-api.php:380 +#: inc/api/class-package-api.php:677 msgid "" "If a package does not exist, upload it by providing a valid package file, or " "download it if using a VCS" @@ -917,17 +921,17 @@ msgid "" "check the provided token has the permissions to access members information." msgstr "" -#: inc/manager/class-api-manager.php:56 +#: inc/manager/class-api-manager.php:85 msgid "" "If you proceed, the Payload URL will not receive the configured events " "anymore." msgstr "" -#: inc/manager/class-api-manager.php:62 +#: inc/manager/class-api-manager.php:91 msgid "If you proceed, the Payload URL will receive events for ALL licenses." msgstr "" -#: inc/manager/class-api-manager.php:50 +#: inc/manager/class-api-manager.php:79 msgid "" "If you proceed, the remote systems using it will not be able to access the " "API anymore." @@ -996,10 +1000,10 @@ msgstr "" msgid "Invalid keys. The following values are valid: %s" msgstr "" -#: inc/api/class-license-api.php:177 inc/api/class-license-api.php:215 -#: inc/api/class-license-api.php:228 inc/api/class-license-api.php:235 -#: inc/api/class-license-api.php:266 inc/api/class-license-api.php:273 -#: inc/api/class-license-api.php:280 inc/api/class-license-api.php:302 +#: inc/api/class-license-api.php:245 inc/api/class-license-api.php:290 +#: inc/api/class-license-api.php:303 inc/api/class-license-api.php:310 +#: inc/api/class-license-api.php:348 inc/api/class-license-api.php:355 +#: inc/api/class-license-api.php:362 inc/api/class-license-api.php:391 msgid "Invalid license data." msgstr "" @@ -1023,11 +1027,11 @@ msgstr "" msgid "Invalid relationship operator. Only \"AND\" and \"OR\" are allowed." msgstr "" -#: inc/api/class-webhook-api.php:419 -msgid "Invalid request signature" +#: inc/api/class-webhook-api.php:623 +msgid "Invalid request" msgstr "" -#: inc/api/class-webhook-api.php:101 +#: inc/api/class-webhook-api.php:158 msgid "Invalid signature" msgstr "" @@ -1073,10 +1077,6 @@ msgid "" "core from executing beyond what is strictly necessary." msgstr "" -#: inc/table/class-packages-table.php:49 -msgid "File Modified " -msgstr "" - #: inc/templates/admin/plugin-remote-sources-page.php:198 #: inc/templates/admin/remote-webhook-options.php:39 msgid "" @@ -1089,15 +1089,15 @@ msgstr "" msgid "Leave blank to allow any IP address (not recommended)." msgstr "" -#: inc/api/class-license-api.php:464 +#: inc/api/class-license-api.php:687 msgid "License activated" msgstr "" -#: inc/api/class-license-api.php:466 +#: inc/api/class-license-api.php:689 msgid "License added" msgstr "" -#: inc/manager/class-license-manager.php:573 +#: inc/manager/class-license-manager.php:844 msgid "License added successfully." msgstr "" @@ -1105,7 +1105,7 @@ msgstr "" msgid "License API" msgstr "" -#: inc/api/class-license-api.php:1145 +#: inc/api/class-license-api.php:1620 msgid "License API action not found." msgstr "" @@ -1113,27 +1113,27 @@ msgstr "" msgid "License creation failed - database insertion error." msgstr "" -#: inc/manager/class-license-manager.php:577 +#: inc/manager/class-license-manager.php:848 msgid "License creation failed." msgstr "" -#: inc/api/class-license-api.php:465 +#: inc/api/class-license-api.php:688 msgid "License deactivated" msgstr "" -#: inc/api/class-license-api.php:468 +#: inc/api/class-license-api.php:691 msgid "License deleted" msgstr "" -#: inc/api/class-license-api.php:467 +#: inc/api/class-license-api.php:690 msgid "License edited" msgstr "" -#: inc/manager/class-license-manager.php:559 +#: inc/manager/class-license-manager.php:822 msgid "License edited successfully." msgstr "" -#: inc/manager/class-api-manager.php:179 +#: inc/manager/class-api-manager.php:262 msgid "License events" msgstr "" @@ -1150,9 +1150,9 @@ msgstr "" msgid "License Key ID" msgstr "" -#: inc/api/class-license-api.php:171 inc/api/class-license-api.php:184 -#: inc/api/class-license-api.php:221 inc/api/class-license-api.php:242 -#: inc/api/class-license-api.php:296 inc/api/class-license-api.php:309 +#: inc/api/class-license-api.php:239 inc/api/class-license-api.php:252 +#: inc/api/class-license-api.php:296 inc/api/class-license-api.php:317 +#: inc/api/class-license-api.php:385 inc/api/class-license-api.php:398 msgid "License not found." msgstr "" @@ -1168,8 +1168,8 @@ msgstr "" msgid "License Status" msgstr "" -#: inc/manager/class-license-manager.php:248 -#: inc/manager/class-license-manager.php:254 +#: inc/manager/class-license-manager.php:384 +#: inc/manager/class-license-manager.php:399 msgid "License status" msgstr "" @@ -1177,21 +1177,21 @@ msgstr "" msgid "License update failed - database update error." msgstr "" -#: inc/manager/class-license-manager.php:563 +#: inc/manager/class-license-manager.php:826 msgid "License update failed." msgstr "" -#: inc/manager/class-license-manager.php:270 -#: inc/manager/class-license-manager.php:281 +#: inc/manager/class-license-manager.php:432 +#: inc/manager/class-license-manager.php:452 #: inc/templates/admin/plugin-licenses-page.php:8 msgid "Licenses" msgstr "" -#: inc/api/class-license-api.php:138 +#: inc/api/class-license-api.php:199 msgid "Licenses not found." msgstr "" -#: inc/manager/class-license-manager.php:235 +#: inc/manager/class-license-manager.php:351 msgid "Licenses per page" msgstr "" @@ -1219,7 +1219,7 @@ msgstr "" msgid "Malformed request" msgstr "" -#: inc/api/class-package-api.php:818 inc/api/class-license-api.php:1159 +#: inc/api/class-package-api.php:1293 inc/api/class-license-api.php:1634 msgid "Malformed request." msgstr "" @@ -1277,11 +1277,11 @@ msgstr "" msgid "No file was uploaded." msgstr "" -#: inc/api/class-package-api.php:69 +#: inc/api/class-package-api.php:139 msgid "No packages found." msgstr "" -#: inc/api/class-update-api.php:299 +#: inc/api/class-update-api.php:497 msgid "No server found for this package." msgstr "" @@ -1296,20 +1296,20 @@ msgid "Not a valid number" msgstr "" #: inc/manager/class-remote-sources-manager.php:658 -#: inc/manager/class-api-manager.php:410 inc/manager/class-api-manager.php:422 -#: inc/manager/class-api-manager.php:434 +#: inc/manager/class-api-manager.php:542 inc/manager/class-api-manager.php:554 +#: inc/manager/class-api-manager.php:566 msgid "Not a valid payload" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:217 -#: inc/manager/class-cloud-storage-manager.php:228 -#: inc/manager/class-cloud-storage-manager.php:239 -#: inc/manager/class-cloud-storage-manager.php:250 -#: inc/manager/class-cloud-storage-manager.php:261 +#: inc/manager/class-cloud-storage-manager.php:326 +#: inc/manager/class-cloud-storage-manager.php:337 +#: inc/manager/class-cloud-storage-manager.php:348 +#: inc/manager/class-cloud-storage-manager.php:359 +#: inc/manager/class-cloud-storage-manager.php:370 msgid "Not a valid string" msgstr "" -#: inc/manager/class-license-manager.php:263 +#: inc/manager/class-license-manager.php:418 msgid "Not Required" msgstr "" @@ -1342,25 +1342,25 @@ msgstr "" msgid "open an issue on Github" msgstr "" -#: inc/manager/class-license-manager.php:523 +#: inc/manager/class-license-manager.php:769 msgid "" "Operation failed: all the selected licenses have passed their expiry date, " "or already have the selected status - IDs: " msgstr "" -#: inc/manager/class-license-manager.php:476 +#: inc/manager/class-license-manager.php:722 msgid "Operation failed: an unexpected error occured (invalid license data)." msgstr "" -#: inc/manager/class-license-manager.php:513 +#: inc/manager/class-license-manager.php:759 msgid "Operation failed: an unexpected error occured (invalid license status)." msgstr "" #. %1$s is the option display name, %2$s is the condition for update #: inc/manager/class-remote-sources-manager.php:477 #: inc/manager/class-package-manager.php:1214 -#: inc/manager/class-license-manager.php:434 -#: inc/manager/class-api-manager.php:377 +#: inc/manager/class-license-manager.php:658 +#: inc/manager/class-api-manager.php:488 #, php-format msgid "Option %1$s was not updated. Reason: %2$s" msgstr "" @@ -1386,22 +1386,22 @@ msgid "Owner Name" msgstr "" #. %1$s: package ID, %2$s: scheduled date and time -#: inc/api/class-webhook-api.php:370 +#: inc/api/class-webhook-api.php:559 #, php-format msgid "Package %1$s has been scheduled for download: %2$s." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:392 +#: inc/api/class-webhook-api.php:587 #, php-format msgid "Package %s downloaded." msgstr "" -#: inc/api/class-package-api.php:390 +#: inc/api/class-package-api.php:696 msgid "Package added or updated" msgstr "" -#: inc/api/class-package-api.php:171 +#: inc/api/class-package-api.php:310 msgid "Package already exists." msgstr "" @@ -1409,31 +1409,31 @@ msgstr "" msgid "Package API" msgstr "" -#: inc/api/class-package-api.php:804 +#: inc/api/class-package-api.php:1279 msgid "Package API action not found." msgstr "" -#: inc/manager/class-api-manager.php:409 +#: inc/manager/class-api-manager.php:541 msgid "Package API Authentication Keys" msgstr "" -#: inc/api/class-package-api.php:183 +#: inc/api/class-package-api.php:322 msgid "Package could not be added - invalid parameters." msgstr "" -#: inc/api/class-package-api.php:139 +#: inc/api/class-package-api.php:253 msgid "Package could not be edited - invalid parameters." msgstr "" -#: inc/api/class-package-api.php:391 +#: inc/api/class-package-api.php:697 msgid "Package deleted" msgstr "" -#: inc/api/class-package-api.php:392 +#: inc/api/class-package-api.php:698 msgid "Package downloaded via a signed URL" msgstr "" -#: inc/manager/class-api-manager.php:175 +#: inc/manager/class-api-manager.php:258 msgid "Package events" msgstr "" @@ -1445,13 +1445,13 @@ msgstr "" msgid "Package Name" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:814 +#: inc/manager/class-cloud-storage-manager.php:1050 msgid "Package not found" msgstr "" -#: inc/api/class-package-api.php:97 inc/api/class-package-api.php:127 -#: inc/api/class-package-api.php:204 inc/api/class-package-api.php:219 -#: inc/api/class-package-api.php:270 +#: inc/api/class-package-api.php:192 inc/api/class-package-api.php:241 +#: inc/api/class-package-api.php:383 inc/api/class-package-api.php:408 +#: inc/api/class-package-api.php:499 msgid "Package not found." msgstr "" @@ -1488,7 +1488,7 @@ msgstr "" msgid "Packages per page" msgstr "" -#: inc/manager/class-license-manager.php:212 +#: inc/manager/class-license-manager.php:315 msgid "" "Packages requiring these licenses will not be able to get a successful " "response from this server." @@ -1503,7 +1503,7 @@ msgid "Pending" msgstr "" #. %1$s is the path to the plugin's data directory -#: inc/manager/class-data-manager.php:51 +#: inc/manager/class-data-manager.php:85 #, php-format msgid "" "Permission errors creating %1$s - could not setup the data directory. Please " @@ -1545,7 +1545,7 @@ msgid "" "Version Control System when calling the Webhook." msgstr "" -#: inc/manager/class-api-manager.php:421 +#: inc/manager/class-api-manager.php:553 msgid "Private API Authentication Key" msgstr "" @@ -1606,11 +1606,11 @@ msgstr "" msgid "Requests optimisation" msgstr "" -#: inc/manager/class-license-manager.php:263 +#: inc/manager/class-license-manager.php:418 msgid "Required" msgstr "" -#: inc/api/class-package-api.php:382 +#: inc/api/class-package-api.php:679 msgid "Retrieve secure URLs for downloading packages" msgstr "" @@ -1656,7 +1656,7 @@ msgstr "" msgid "Select a VCS" msgstr "" -#: inc/manager/class-license-manager.php:548 +#: inc/manager/class-license-manager.php:803 msgid "Selected licenses deleted - IDs: " msgstr "" @@ -1677,7 +1677,7 @@ msgstr "" msgid "Send a test request to the Version Control System." msgstr "" -#: inc/table/class-licenses-table.php:256 +#: inc/table/class-licenses-table.php:260 msgid "Set to Pending" msgstr "" @@ -1691,7 +1691,7 @@ msgstr "" msgid "Size" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:206 +#: inc/manager/class-cloud-storage-manager.php:315 msgid "Something went wrong" msgstr "" @@ -1699,7 +1699,7 @@ msgstr "" msgid "Status" msgstr "" -#: inc/manager/class-license-manager.php:521 +#: inc/manager/class-license-manager.php:767 msgid "" "Status of the selected licenses updated successfully where applicable - IDs " "of updated licenses: " @@ -1825,40 +1825,40 @@ msgid "The ID must be an integer." msgstr "" #. %s is the license key -#: inc/api/class-license-api.php:523 +#: inc/api/class-license-api.php:759 #, php-format msgid "The license `%s` has been activated on UpdatePulse Server" msgstr "" #. %s is the license key -#: inc/api/class-license-api.php:513 +#: inc/api/class-license-api.php:749 #, php-format msgid "The license `%s` has been added on UpdatePulse Server" msgstr "" #. %s is the license key -#: inc/api/class-license-api.php:528 +#: inc/api/class-license-api.php:764 #, php-format msgid "The license `%s` has been deactivated on UpdatePulse Server" msgstr "" #. %s is the license key -#: inc/api/class-license-api.php:518 +#: inc/api/class-license-api.php:754 #, php-format msgid "The license `%s` has been deleted on UpdatePulse Server" msgstr "" #. %s is the license key -#: inc/api/class-license-api.php:508 +#: inc/api/class-license-api.php:744 #, php-format msgid "The license `%s` has been edited on UpdatePulse Server" msgstr "" -#: inc/api/class-license-api.php:830 +#: inc/api/class-license-api.php:1178 msgid "The license cannot be activated due to its current status." msgstr "" -#: inc/api/class-license-api.php:936 +#: inc/api/class-license-api.php:1341 msgid "The license cannot be deactivated before the specified date." msgstr "" @@ -1867,15 +1867,15 @@ msgstr "" msgid "The license cannot be found." msgstr "" -#: inc/api/class-license-api.php:846 +#: inc/api/class-license-api.php:1201 msgid "The license has reached the maximum allowed activations for domains." msgstr "" -#: inc/api/class-license-api.php:856 +#: inc/api/class-license-api.php:1218 msgid "The license is already activated for the specified domain." msgstr "" -#: inc/api/class-license-api.php:926 +#: inc/api/class-license-api.php:1324 msgid "The license is already deactivated for the specified domain." msgstr "" @@ -1920,14 +1920,14 @@ msgid "The Package Key ID must contain only numbers, letters, %1$s and %2$s." msgstr "" #. %s is the package slug -#: inc/api/class-package-api.php:369 +#: inc/api/class-package-api.php:657 #, php-format msgid "" "The package of `%s` has been securely downloaded from UpdatePulse Server" msgstr "" #. %1$s is the package type, %2$s is the package slug -#: inc/api/class-package-api.php:357 inc/manager/class-package-manager.php:552 +#: inc/api/class-package-api.php:637 inc/manager/class-package-manager.php:552 #, php-format msgid "" "The package of type `%1$s` and slug `%2$s` has been deleted on UpdatePulse " @@ -1935,7 +1935,7 @@ msgid "" msgstr "" #. %1$s is the package type, %2$s is the pakage slug -#: inc/api/class-package-api.php:332 +#: inc/api/class-package-api.php:593 #, php-format msgid "" "The package of type `%1$s` and slug `%2$s` has been updated on UpdatePulse " @@ -1966,23 +1966,23 @@ msgid "" "dashes are allowed." msgstr "" -#: inc/api/class-package-api.php:577 +#: inc/api/class-package-api.php:976 msgid "The provided file does not match the provided hash." msgstr "" -#: inc/api/class-package-api.php:626 +#: inc/api/class-package-api.php:1025 msgid "The provided file is not a valid package." msgstr "" -#: inc/api/class-package-api.php:596 +#: inc/api/class-package-api.php:995 msgid "The provided file is not a valid ZIP file." msgstr "" -#: inc/api/class-license-api.php:991 +#: inc/api/class-license-api.php:1425 msgid "The provided license key is invalid." msgstr "" -#: functions.php:277 inc/api/class-package-api.php:650 +#: functions.php:560 inc/api/class-package-api.php:1049 msgid "The provided VCS information is not valid" msgstr "" @@ -2087,7 +2087,7 @@ msgid "" " and NOT IN operators." msgstr "" -#: functions.php:608 inc/api/class-webhook-api.php:141 +#: functions.php:1138 inc/api/class-webhook-api.php:235 msgid "The webhook payload must contain an event string and a content." msgstr "" @@ -2103,8 +2103,8 @@ msgstr "" #: inc/manager/class-remote-sources-manager.php:431 #: inc/manager/class-package-manager.php:1183 -#: inc/manager/class-license-manager.php:408 -#: inc/manager/class-api-manager.php:199 +#: inc/manager/class-license-manager.php:632 +#: inc/manager/class-api-manager.php:290 msgid "" "There was an error validating the form. It may be outdated. Please reload " "the page." @@ -2117,7 +2117,7 @@ msgid "" "UpdatePulse Server is uninstalled." msgstr "" -#: inc/api/class-license-api.php:1005 +#: inc/api/class-license-api.php:1439 msgid "This is an unexpected error. Please contact support." msgstr "" @@ -2186,12 +2186,12 @@ msgid "" "editing." msgstr "" -#: inc/api/class-package-api.php:811 inc/api/class-license-api.php:1152 +#: inc/api/class-package-api.php:1286 inc/api/class-license-api.php:1627 #: inc/nonce/class-nonce.php:96 msgid "Unauthorized access." msgstr "" -#: inc/api/class-package-api.php:751 inc/api/class-license-api.php:1096 +#: inc/api/class-package-api.php:1210 inc/api/class-license-api.php:1556 msgid "Unauthorized GET method." msgstr "" @@ -2199,7 +2199,7 @@ msgstr "" msgid "Unavailable file system." msgstr "" -#: functions.php:31 +#: functions.php:52 msgid "Undefined" msgstr "" @@ -2213,9 +2213,9 @@ msgstr "" msgid "Unknown" msgstr "" -#: inc/api/class-license-api.php:214 inc/api/class-license-api.php:234 -#: inc/api/class-license-api.php:265 inc/api/class-license-api.php:279 -#: inc/api/class-license-api.php:998 +#: inc/api/class-license-api.php:289 inc/api/class-license-api.php:309 +#: inc/api/class-license-api.php:347 inc/api/class-license-api.php:361 +#: inc/api/class-license-api.php:1432 msgid "Unknown error." msgstr "" @@ -2231,7 +2231,7 @@ msgid "" "file in %2$s would be %3$s." msgstr "" -#: inc/api/class-license-api.php:454 +#: inc/api/class-license-api.php:670 msgid "Update license records" msgstr "" @@ -2244,7 +2244,7 @@ msgstr "" msgid "UpdatePulse Server" msgstr "" -#: inc/manager/class-api-manager.php:98 +#: inc/manager/class-api-manager.php:134 msgid "UpdatePulse Server - API & Webhooks" msgstr "" @@ -2252,7 +2252,7 @@ msgstr "" msgid "UpdatePulse Server - Help" msgstr "" -#: inc/manager/class-license-manager.php:269 +#: inc/manager/class-license-manager.php:431 msgid "UpdatePulse Server - Licenses" msgstr "" @@ -2266,28 +2266,28 @@ msgstr "" msgid "UpdatePulse Server Integration" msgstr "" -#: inc/manager/class-license-manager.php:415 +#: inc/manager/class-license-manager.php:639 msgid "UpdatePulse Server license options successfully updated." msgstr "" #: inc/manager/class-remote-sources-manager.php:438 #: inc/manager/class-package-manager.php:1190 -#: inc/manager/class-api-manager.php:206 +#: inc/manager/class-api-manager.php:297 msgid "UpdatePulse Server options successfully updated" msgstr "" #. the separator between summaries; example: All package events, 3 license events -#: inc/manager/class-api-manager.php:84 +#: inc/manager/class-api-manager.php:113 msgctxt "UpdatePulse Server separator between API summaries" msgid ", " msgstr "" -#: inc/manager/class-api-manager.php:82 +#: inc/manager/class-api-manager.php:111 msgctxt "UpdatePulse Server webhook event type" msgid "license" msgstr "" -#: inc/manager/class-api-manager.php:81 +#: inc/manager/class-api-manager.php:110 msgctxt "UpdatePulse Server webhook event type" msgid "package" msgstr "" @@ -2300,7 +2300,7 @@ msgstr "" msgid "Upload package" msgstr "" -#: inc/manager/class-cloud-storage-manager.php:205 +#: inc/manager/class-cloud-storage-manager.php:314 #: inc/templates/admin/cloud-storage-options.php:8 msgid "Use Cloud Storage" msgstr "" @@ -2371,13 +2371,13 @@ msgid "Version Control Systems " msgstr "" #. %s is the virtual folder -#: inc/manager/class-cloud-storage-manager.php:509 +#: inc/manager/class-cloud-storage-manager.php:673 #, php-format msgid "Virtual folder \"%s\" found." msgstr "" #. %s is the virtual folder -#: inc/manager/class-cloud-storage-manager.php:498 +#: inc/manager/class-cloud-storage-manager.php:662 #, php-format msgid "Virtual folder \"%s\" was created successfully." msgstr "" @@ -2407,8 +2407,8 @@ msgid "" msgstr "" #. %s is the virtual folder -#: inc/manager/class-cloud-storage-manager.php:503 -#: inc/manager/class-cloud-storage-manager.php:551 +#: inc/manager/class-cloud-storage-manager.php:667 +#: inc/manager/class-cloud-storage-manager.php:722 #, php-format msgid "" "WARNING: Unable to create Virtual folder \"%s\". The Cloud Storage feature " @@ -2432,7 +2432,7 @@ msgid "" "and %6$s is the slug of the package to register." msgstr "" -#: inc/manager/class-api-manager.php:433 +#: inc/manager/class-api-manager.php:565 #: inc/templates/admin/plugin-api-page.php:140 #: inc/templates/admin/plugin-api-page.php:140 msgid "Webhooks" @@ -2475,15 +2475,15 @@ msgid "" "also loaded, with its own plugins and themes." msgstr "" -#: inc/manager/class-api-manager.php:61 +#: inc/manager/class-api-manager.php:90 msgid "You are about to add a Webhook without License API Key ID." msgstr "" -#: inc/manager/class-api-manager.php:55 +#: inc/manager/class-api-manager.php:84 msgid "You are about to delete a Webhook." msgstr "" -#: inc/manager/class-license-manager.php:210 +#: inc/manager/class-license-manager.php:313 msgid "You are about to delete all the licenses from this server." msgstr "" @@ -2492,7 +2492,7 @@ msgstr "" msgid "You are about to delete all the packages from this server." msgstr "" -#: inc/manager/class-api-manager.php:49 +#: inc/manager/class-api-manager.php:78 msgid "You are about to delete an API key." msgstr "" From 3b7f404f7ffcc1322ae86f0e6bf475292016a771 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:20:28 +0800 Subject: [PATCH 28/49] fix doc --- inc/api/class-webhook-api.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 4b735f2..5d78ecb 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -14,46 +14,46 @@ use Anyape\UpdatePulse\Server\Scheduler\Scheduler; use Anyape\Utils\Utils; /** - *Webhook API class + * Webhook API class * - *@since 1.0.0 + * @since 1.0.0 */ class Webhook_API { /** - *Is doing API request + * Is doing API request * - *@var bool|null + * @var bool|null */ protected static $doing_api_request = null; /** - *Instance + * Instance * - *@var Webhook_API|null + * @var Webhook_API|null */ protected static $instance; /** - *Webhooks configuration + * Webhooks configuration * - *@var array + * @var array */ protected $webhooks; /** - *HTTP response code + * HTTP response code * - *@var int + * @var int */ protected $http_response_code = 200; /** - *Constructor + * Constructor * - *@since 1.0.0 + * @since 1.0.0 * - *@param boolean $init_hooks Whether to initialize hooks + * @param boolean $init_hooks Whether to initialize hooks */ public function __construct( $init_hooks = false ) { $this->webhooks = upserv_get_option( 'api/webhooks', array() ); From 9cd489575efd7d6102982e93afecc9bc614f6ed6 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:22:49 +0800 Subject: [PATCH 29/49] fix doc --- inc/api/class-webhook-api.php | 118 +++++++++++++++++----------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 5d78ecb..2d4578d 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -85,15 +85,15 @@ class Webhook_API { } /******************************************************************* - *Public methods + * Public methods *******************************************************************/ // WordPress hooks --------------------------------------------- /** - *Add API endpoints + * Add API endpoints * - *Register the rewrite rules for the Webhook API endpoints. + * Register the rewrite rules for the Webhook API endpoints. */ public function add_endpoints() { add_rewrite_rule( '^updatepulse-server-webhook$', 'index.php?__upserv_webhook=1&', 'top' ); @@ -105,9 +105,9 @@ class Webhook_API { } /** - *Parse API requests + * Parse API requests * - *Handle incoming API requests to the Webhook API endpoints. + * Handle incoming API requests to the Webhook API endpoints. */ public function parse_request() { global $wp; @@ -120,12 +120,12 @@ class Webhook_API { } /** - *Register query variables + * Register query variables * - *Add custom query variables used by the Webhook API. + * Add custom query variables used by the Webhook API. * - *@param array $query_vars Existing query variables. - *@return array Modified query variables. + * @param array $query_vars Existing query variables. + * @return array Modified query variables. */ public function query_vars( $query_vars ) { $query_vars = array_merge( @@ -141,9 +141,9 @@ class Webhook_API { } /** - *Handle invalid webhook requests + * Handle invalid webhook requests * - *Display error page for unauthorized webhook requests. + * Display error page for unauthorized webhook requests. */ public function upserv_webhook_invalid_request() { $protocol = empty( $_SERVER['SERVER_PROTOCOL'] ) ? 'HTTP/1.1' : sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); @@ -163,17 +163,17 @@ class Webhook_API { } /** - *Process webhook requests + * Process webhook requests * - *Determine whether to process webhook requests based on branch matching. + * Determine whether to process webhook requests based on branch matching. * - *@param bool $process Current process status. - *@param array $payload Request payload. - *@param string $slug Package slug. - *@param string $type Package type. - *@param bool $package_exists Whether package already exists. - *@param array $vcs_config Version control system configuration. - *@return bool Whether to process the webhook request. + * @param bool $process Current process status. + * @param array $payload Request payload. + * @param string $slug Package slug. + * @param string $type Package type. + * @param bool $package_exists Whether package already exists. + * @param array $vcs_config Version control system configuration. + * @return bool Whether to process the webhook request. */ public function upserv_webhook_process_request( $process, $payload, $slug, $type, $package_exists, $vcs_config ) { return $this->get_payload_vcs_branch( $payload ) === $vcs_config['branch']; @@ -182,11 +182,11 @@ class Webhook_API { // Misc. ------------------------------------------------------- /** - *Check if currently processing an API request + * Check if currently processing an API request * - *Determine whether the current request is a Webhook API request. + * Determine whether the current request is a Webhook API request. * - *@return bool Whether the current request is a Webhook API request. + * @return bool Whether the current request is a Webhook API request. */ public static function is_doing_api_request() { @@ -198,11 +198,11 @@ class Webhook_API { } /** - *Get Webhook API instance + * Get Webhook API instance * - *Retrieve or create the Webhook API singleton instance. + * Retrieve or create the Webhook API singleton instance. * - *@return Webhook_API The Webhook API instance. + * @return Webhook_API The Webhook API instance. */ public static function get_instance() { @@ -214,14 +214,14 @@ class Webhook_API { } /** - *Schedule webhook + * Schedule webhook * - *Schedule a webhook to be fired based on an event. + * Schedule a webhook to be fired based on an event. * - *@param array $payload Webhook payload data. - *@param string $event_type Event type identifier. - *@param bool $instant Whether to fire webhook immediately. - *@return void|WP_Error WP_Error on failure. + * @param array $payload Webhook payload data. + * @param string $event_type Event type identifier. + * @param bool $instant Whether to fire webhook immediately. + * @return void|WP_Error WP_Error on failure. */ public function schedule_webhook( $payload, $event_type, $instant = false ) { @@ -306,15 +306,15 @@ class Webhook_API { } /** - *Fire webhook + * Fire webhook * - *Send an HTTP request to the webhook endpoint. + * Send an HTTP request to the webhook endpoint. * - *@param string $url Webhook endpoint URL. - *@param string $secret Secret key for signature. - *@param string $body Request body. - *@param string $action Current action. - *@return array|WP_Error HTTP response or WP_Error on failure. + * @param string $url Webhook endpoint URL. + * @param string $secret Secret key for signature. + * @param string $body Request body. + * @param string $action Current action. + * @return array|WP_Error HTTP response or WP_Error on failure. */ public function fire_webhook( $url, $secret, $body, $action ) { return wp_remote_post( @@ -332,13 +332,13 @@ class Webhook_API { } /******************************************************************* - *Protected methods + * Protected methods *******************************************************************/ /** - *Handle remote test + * Handle remote test * - *Process and respond to webhook test requests. + * Process and respond to webhook test requests. */ protected function handle_remote_test() { @@ -394,9 +394,9 @@ class Webhook_API { } /** - *Handle API request + * Handle API request * - *Process webhook API requests and return appropriate responses. + * Process webhook API requests and return appropriate responses. */ protected function handle_api_request() { global $wp; @@ -656,12 +656,12 @@ class Webhook_API { } /** - *Validate webhook request + * Validate webhook request * - *Verify webhook request signature against stored secrets. + * Verify webhook request signature against stored secrets. * - *@param array $vcs_config Version control system configuration. - *@return bool Whether the request signature is valid. + * @param array $vcs_config Version control system configuration. + * @return bool Whether the request signature is valid. */ protected function validate_request( $vcs_config ) { $valid = false; @@ -732,11 +732,11 @@ class Webhook_API { } /** - *Get webhook payload + * Get webhook payload * - *Extract and decode the payload from the webhook request. + * Extract and decode the payload from the webhook request. * - *@return array Decoded webhook payload. + * @return array Decoded webhook payload. */ protected function get_payload() { $payload = @file_get_contents( 'php://input' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.NoSilencedErrors.Discouraged @@ -756,12 +756,12 @@ class Webhook_API { } /** - *Get VCS URL from payload + * Get VCS URL from payload * - *Extract the version control system URL from webhook payload. + * Extract the version control system URL from webhook payload. * - *@param array $payload Webhook payload. - *@return string|false VCS URL or false if not found. + * @param array $payload Webhook payload. + * @return string|false VCS URL or false if not found. */ protected function get_payload_vcs_url( $payload ) { $url = false; @@ -809,12 +809,12 @@ class Webhook_API { } /** - *Get VCS branch from payload + * Get VCS branch from payload * - *Extract the branch information from webhook payload. + * Extract the branch information from webhook payload. * - *@param array $payload Webhook payload. - *@return string|false Branch name or false if not found. + * @param array $payload Webhook payload. + * @return string|false Branch name or false if not found. */ protected function get_payload_vcs_branch( $payload ) { $branch = false; From 791ca0559e555c6868a04007e050520dba69f46c Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:24:59 +0800 Subject: [PATCH 30/49] See https://github.com/Anyape/updatepulse-server/issues/4 --- README.md | 2 +- inc/templates/admin/plugin-remote-sources-page.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62ae54a..233e963 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ VCS URL | text | The URL of the Version Control Syste Self-hosted VCS | checkbox | Check this only if the Version Control System is a self-hosted instance of Gitlab. Packages branch name | text | The branch to download when getting remote packages from the Version Control System. VCS credentials | text | Credentials for non-publicly accessible repositories.
In the case of Github and Gitlab, a Personal Access Token; in the case of Bitckucket, an App Password.
**WARNING: Keep these credentials secret, do not share them, and take care of renewing them before they expire!** -Use Webhooks | checkbox | Check so that each repository of the Version Control System calls a Webhook when updates are pushed.
When checked, UpdatePulse Server will not regularly poll repositories for package version changes, but relies on events sent by the repositories to schedule a package download.
Webhook URL: `https://domain.tld/updatepulse-server-webhook/package-type/package-slug` - where `package-type` is the package type (`plugin`, `theme`, or `generic`) and `package-slug` is the slug of the package that needs updates.
Note that UpdatePulse Server does not rely on the content of the payload to schedule a package download, so any type of event can be used to trigger the Webhook. +Use Webhooks | checkbox | Check so that each repository of the Version Control System calls a Webhook when updates are pushed.
When checked, UpdatePulse Server will not regularly poll repositories for package version changes, but relies on events sent by the repositories to schedule a package download.
Webhook URL: `https://domain.tld/updatepulse-server-webhook/package-type/package-slug` - where `package-type` is the package type (`plugin`, `theme`, or `generic`) and `package-slug` is the slug of the package that needs updates.
Note that UpdatePulse Server relies on the `push` event content of the payload to schedule a package download. Remote Download Delay | number | Delay in minutes after which UpdatePulse Server will poll the Version Control System for package updates when the Webhook has been called.
Leave at `0` to schedule a package update during the cron run happening immediately after the Webhook notification was received. VCS Webhook Secret | text | Ideally a random string, the secret string included in the request by the repository service when calling the Webhook.
**WARNING: Changing this value will invalidate all the existing Webhooks set up on all package repositories.**
After changing this setting, make sure to update the Webhooks secrets in the repository service. Remote update check frequency | select | Only available in case Webhooks are not used - How often UpdatePulse Server will poll each Version Control System for package updates - checking too often may slow down the server (recommended "Once Daily"). diff --git a/inc/templates/admin/plugin-remote-sources-page.php b/inc/templates/admin/plugin-remote-sources-page.php index 243cfd4..050d3b0 100644 --- a/inc/templates/admin/plugin-remote-sources-page.php +++ b/inc/templates/admin/plugin-remote-sources-page.php @@ -183,7 +183,7 @@ ); ?>
- +

From 452184d062f01858eeb9aea36da4e1656d4269ac Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:27:15 +0800 Subject: [PATCH 31/49] Fix https://github.com/Anyape/updatepulse-server/issues/4#issuecomment-2738822390 --- inc/manager/class-remote-sources-manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/manager/class-remote-sources-manager.php b/inc/manager/class-remote-sources-manager.php index 55efd51..7bdcdb2 100644 --- a/inc/manager/class-remote-sources-manager.php +++ b/inc/manager/class-remote-sources-manager.php @@ -89,7 +89,7 @@ class Remote_Sources_Manager { foreach ( $vcs_configs as $vcs_c ) { - if ( $vcs_c['use_webhooks'] || ! isset( $vcs_c['url'] ) ) { + if ( ! isset( $vcs_c['url'] ) ) { continue; } From ae4cc6e0e245c08c9b959e545686879fc923f7ba Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:28:55 +0800 Subject: [PATCH 32/49] Use a VCS candidates system in case of unspecified branch in the webhook event to support all events, not just push https://github.com/Anyape/updatepulse-server/issues/4 --- README.md | 2 +- inc/api/class-webhook-api.php | 101 +++++++++++++++--- .../admin/plugin-remote-sources-page.php | 2 +- languages/updatepulse-server.pot | 22 ++-- 4 files changed, 101 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 233e963..62ae54a 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ VCS URL | text | The URL of the Version Control Syste Self-hosted VCS | checkbox | Check this only if the Version Control System is a self-hosted instance of Gitlab. Packages branch name | text | The branch to download when getting remote packages from the Version Control System. VCS credentials | text | Credentials for non-publicly accessible repositories.
In the case of Github and Gitlab, a Personal Access Token; in the case of Bitckucket, an App Password.
**WARNING: Keep these credentials secret, do not share them, and take care of renewing them before they expire!** -Use Webhooks | checkbox | Check so that each repository of the Version Control System calls a Webhook when updates are pushed.
When checked, UpdatePulse Server will not regularly poll repositories for package version changes, but relies on events sent by the repositories to schedule a package download.
Webhook URL: `https://domain.tld/updatepulse-server-webhook/package-type/package-slug` - where `package-type` is the package type (`plugin`, `theme`, or `generic`) and `package-slug` is the slug of the package that needs updates.
Note that UpdatePulse Server relies on the `push` event content of the payload to schedule a package download. +Use Webhooks | checkbox | Check so that each repository of the Version Control System calls a Webhook when updates are pushed.
When checked, UpdatePulse Server will not regularly poll repositories for package version changes, but relies on events sent by the repositories to schedule a package download.
Webhook URL: `https://domain.tld/updatepulse-server-webhook/package-type/package-slug` - where `package-type` is the package type (`plugin`, `theme`, or `generic`) and `package-slug` is the slug of the package that needs updates.
Note that UpdatePulse Server does not rely on the content of the payload to schedule a package download, so any type of event can be used to trigger the Webhook. Remote Download Delay | number | Delay in minutes after which UpdatePulse Server will poll the Version Control System for package updates when the Webhook has been called.
Leave at `0` to schedule a package update during the cron run happening immediately after the Webhook notification was received. VCS Webhook Secret | text | Ideally a random string, the secret string included in the request by the repository service when calling the Webhook.
**WARNING: Changing this value will invalidate all the existing Webhooks set up on all package repositories.**
After changing this setting, make sure to update the Webhooks secrets in the repository service. Remote update check frequency | select | Only available in case Webhooks are not used - How often UpdatePulse Server will poll each Version Control System for package updates - checking too often may slow down the server (recommended "Once Daily"). diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 2d4578d..9392ac4 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -166,6 +166,8 @@ class Webhook_API { * Process webhook requests * * Determine whether to process webhook requests based on branch matching. + * If no branch is specified, the request will be processed to account for events + * registered to the webhook that do not have a branch associated with them. * * @param bool $process Current process status. * @param array $payload Request payload. @@ -176,7 +178,9 @@ class Webhook_API { * @return bool Whether to process the webhook request. */ public function upserv_webhook_process_request( $process, $payload, $slug, $type, $package_exists, $vcs_config ) { - return $this->get_payload_vcs_branch( $payload ) === $vcs_config['branch']; + $branch = $this->get_payload_vcs_branch( $payload ); + + return $process && ( $branch === $vcs_config['branch'] || ! $branch ); } // Misc. ------------------------------------------------------- @@ -405,13 +409,29 @@ class Webhook_API { $this->handle_remote_test(); } - $response = array(); - $payload = $this->get_payload(); - $url = $this->get_payload_vcs_url( $payload ); - $branch = $this->get_payload_vcs_branch( $payload ); - $vcs_configs = upserv_get_option( 'vcs', array() ); - $vcs_key = hash( 'sha256', trailingslashit( $url ) . '|' . $branch ); - $vcs_config = isset( $vcs_configs[ $vcs_key ] ) ? $vcs_configs[ $vcs_key ] : false; + $response = array(); + $payload = $this->get_payload(); + $url = $this->get_payload_vcs_url( $payload ); + $branch = $this->get_payload_vcs_branch( $payload ); + $vcs_configs = upserv_get_option( 'vcs', array() ); + $vcs_key = hash( 'sha256', trailingslashit( $url ) . '|' . $branch ); + $vcs_config = isset( $vcs_configs[ $vcs_key ] ) ? $vcs_configs[ $vcs_key ] : false; + $vcs_candidates = $vcs_config ? array( $vcs_key => $vcs_config ) : array(); + + if ( empty( $vcs_candidates ) ) { + + foreach ( $vcs_configs as $config ) { + + if ( 0 === strpos( $config['url'], trailingslashit( $url ) ) ) { + $vcs_candidates[] = $config; + $vcs_candidates[] = $config; + } + } + } + + if ( 1 === count( $vcs_candidates ) ) { + $vcs_config = reset( $vcs_candidates ); + } /** * Fired before handling a webhook request; fired whether it will be processed or not. @@ -420,7 +440,7 @@ class Webhook_API { */ do_action( 'upserv_webhook_before_handling_request', $vcs_config ); - if ( $this->validate_request( $vcs_config ) ) { + if ( $vcs_config && $this->validate_request( $vcs_config ) ) { $slug = isset( $wp->query_vars['slug'] ) ? trim( rawurldecode( $wp->query_vars['slug'] ) ) : null; @@ -616,13 +636,30 @@ class Webhook_API { $vcs_config ); } - } else { + } elseif ( empty( $vcs_candidates ) ) { $this->http_response_code = 403; $response = array( - 'code' => 'unauthorized', + 'code' => 'invalid_request', 'message' => __( 'Invalid request', 'updatepulse-server' ), ); - + } elseif ( 1 < count( $vcs_candidates ) ) { + $this->http_response_code = 409; + $response = array( + 'code' => 'conflict', + 'message' => __( 'Multiple candidate VCS configurations found ; the event has not be processed. Please limit the events sent to the webhook to events specifying the branch in their payload (such as push), or update your UpdatePulse Server VCS configuration to avoid branch conflicts.', 'updatepulse-server' ), + 'details' => array( + 'vcs_candidates' => array_map( + function ( $config ) { + return array( + 'url' => $config['url'], + 'branch' => $config['branch'], + ); + }, + $vcs_candidates + ), + ), + ); + } else { /** * Fired when a webhook request is invalid. * @@ -631,9 +668,7 @@ class Webhook_API { do_action( 'upserv_webhook_invalid_request', $vcs_config ); } - if ( 200 === $this->http_response_code ) { - $response['time_elapsed'] = Utils::get_time_elapsed(); - } + $response['time_elapsed'] = Utils::get_time_elapsed(); /** * Filter the response data to send to the Version Control System after handling the webhook request. @@ -666,7 +701,7 @@ class Webhook_API { protected function validate_request( $vcs_config ) { $valid = false; $sign = false; - $secret = $vcs_config && isset( $vcs_config['webhook_secret'] ) ? $vcs_config['webhook_secret'] : false; + $secret = isset( $vcs_config['webhook_secret'] ) ? $vcs_config['webhook_secret'] : false; /** * Filter the webhook secret used for request validation. @@ -677,7 +712,7 @@ class Webhook_API { */ $secret = apply_filters( 'upserv_webhook_secret', $secret, $vcs_config ); - if ( ! $vcs_config || ! $secret ) { + if ( ! $secret ) { /** * Filter whether the webhook request is valid after validation. * @@ -833,8 +868,40 @@ class Webhook_API { '', $payload['push']['changes'][0]['new']['name'] ); + } elseif ( isset( $payload['ref'] ) ) { + $branch = str_replace( 'refs/heads/', '', $payload['ref'] ); + } else { + $branch = $this->find_branch_recursively( $payload ); } return $branch; } + + /** + * Recursively search for branch references in payload + * + * Search through nested arrays to find values starting with 'refs/heads/'. + * + * @param mixed $data Part of the payload to search through. + * @return string|false Branch name or false if not found. + */ + protected function find_branch_recursively( $data ) { + + if ( is_string( $data ) && 0 === strpos( $data, 'refs/heads/' ) ) { + return str_replace( 'refs/heads/', '', $data ); + } + + if ( is_array( $data ) ) { + + foreach ( $data as $value ) { + $result = $this->find_branch_recursively( $value ); + + if ( false === $result ) { + return $result; + } + } + } + + return false; + } } diff --git a/inc/templates/admin/plugin-remote-sources-page.php b/inc/templates/admin/plugin-remote-sources-page.php index 050d3b0..243cfd4 100644 --- a/inc/templates/admin/plugin-remote-sources-page.php +++ b/inc/templates/admin/plugin-remote-sources-page.php @@ -183,7 +183,7 @@ ); ?>
- +

diff --git a/languages/updatepulse-server.pot b/languages/updatepulse-server.pot index 55af109..d2eb58c 100644 --- a/languages/updatepulse-server.pot +++ b/languages/updatepulse-server.pot @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: UpdatePulse Server\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-20 00:10+0000\n" +"POT-Creation-Date: 2025-03-20 03:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: \n" @@ -722,7 +722,7 @@ msgid "Failed to create the necessary database table(s)." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:595 +#: inc/api/class-webhook-api.php:615 #, php-format msgid "Failed to download package %s." msgstr "" @@ -732,7 +732,7 @@ msgid "Failed to insert the license record in the database." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:568 +#: inc/api/class-webhook-api.php:588 #, php-format msgid "Failed to sechedule download for package %s." msgstr "" @@ -1027,7 +1027,7 @@ msgstr "" msgid "Invalid relationship operator. Only \"AND\" and \"OR\" are allowed." msgstr "" -#: inc/api/class-webhook-api.php:623 +#: inc/api/class-webhook-api.php:643 msgid "Invalid request" msgstr "" @@ -1258,6 +1258,14 @@ msgstr "" msgid "More help..." msgstr "" +#: inc/api/class-webhook-api.php:649 +msgid "" +"Multiple candidate VCS configurations found ; the event has not be processed." +" Please limit the events sent to the webhook to events specifying the branch " +"in their payload (such as push), or update your UpdatePulse Server VCS " +"configuration to avoid branch conflicts." +msgstr "" + #. %1$s is https://version-control-system.tld/identifier/, %2$s is identifier #: inc/templates/admin/plugin-remote-sources-page.php:74 #: inc/templates/admin/plugin-remote-sources-page.php:274 @@ -1386,13 +1394,13 @@ msgid "Owner Name" msgstr "" #. %1$s: package ID, %2$s: scheduled date and time -#: inc/api/class-webhook-api.php:559 +#: inc/api/class-webhook-api.php:579 #, php-format msgid "Package %1$s has been scheduled for download: %2$s." msgstr "" #. %s: package ID -#: inc/api/class-webhook-api.php:587 +#: inc/api/class-webhook-api.php:607 #, php-format msgid "Package %s downloaded." msgstr "" @@ -2087,7 +2095,7 @@ msgid "" " and NOT IN operators." msgstr "" -#: functions.php:1138 inc/api/class-webhook-api.php:235 +#: functions.php:1138 inc/api/class-webhook-api.php:239 msgid "The webhook payload must contain an event string and a content." msgstr "" From 87aec68366c57ed3dd2fa5bd33bc89704f6ebc42 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:37:46 +0800 Subject: [PATCH 33/49] changelog update --- readme.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.txt b/readme.txt index 49ef056..4e90486 100644 --- a/readme.txt +++ b/readme.txt @@ -121,6 +121,9 @@ This section describes how to install the plugin and get it working. = 1.0.6 = * Fix webhook payload handling (thanks @eHtmlu on github) +* Fix webhook payload scheduling (thanks @BabaYaga0179 on github) +* Implement a VCS candidates logic to handle events that do not specify a branch; gracefully fail with a message in the response if multiple candidates are found +* Major in-code and .md documentation improvements = 1.0.5 = * Fix JSON details modal view - escaping characters From 15a3ffdaaaccb6f1b2888a45ad624eb87c53419c Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:31:25 +0800 Subject: [PATCH 34/49] Full documentation WIP --- inc/manager/class-package-manager.php | 658 ++++++++++++++++++- inc/manager/class-remote-sources-manager.php | 211 +++++- inc/manager/class-zip-package-manager.php | 89 ++- inc/nonce/class-nonce.php | 358 +++++++++- inc/scheduler/class-scheduler.php | 111 ++++ inc/server/license/class-license-server.php | 333 +++++++++- inc/server/update/class-cache.php | 48 +- inc/server/update/class-headers.php | 108 ++- inc/server/update/class-package.php | 48 +- inc/server/update/class-request.php | 125 +++- 10 files changed, 2011 insertions(+), 78 deletions(-) diff --git a/inc/manager/class-package-manager.php b/inc/manager/class-package-manager.php index e13ef51..fdc28fe 100644 --- a/inc/manager/class-package-manager.php +++ b/inc/manager/class-package-manager.php @@ -20,22 +20,77 @@ use Anyape\UpdatePulse\Server\API\Package_API; use Anyape\UpdatePulse\Server\Table\Packages_Table; use Anyape\Utils\Utils; +/** + * Package Manager class + * + * @since 1.0.0 + */ class Package_Manager { - const DEFAULT_LOGS_MAX_SIZE = 10; - const DEFAULT_CACHE_MAX_SIZE = 100; + /** + * Default logs maximum size in MB + * + * @var int + * @since 1.0.0 + */ + const DEFAULT_LOGS_MAX_SIZE = 10; + /** + * Default cache maximum size in MB + * + * @var int + * @since 1.0.0 + */ + const DEFAULT_CACHE_MAX_SIZE = 100; + /** + * Default archive maximum size in MB + * + * @var int + * @since 1.0.0 + */ const DEFAULT_ARCHIVE_MAX_SIZE = 20; + /** + * Filesystem clean types + * + * Types of filesystem data that can be cleaned. + * + * @var array + * @since 1.0.0 + */ public static $filesystem_clean_types = array( 'cache', 'logs', ); + /** + * Instance + * + * @var Package_Manager|null + * @since 1.0.0 + */ protected static $instance; + /** + * Packages table + * + * @var Packages_Table|null + * @since 1.0.0 + */ protected $packages_table; + /** + * Package rows + * + * @var array + * @since 1.0.0 + */ protected $rows = array(); + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize WordPress hooks. + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -63,6 +118,13 @@ class Package_Manager { // WordPress hooks --------------------------------------------- + /** + * Admin init hook + * + * Handles admin initialization tasks. + * + * @since 1.0.0 + */ public function admin_init() { if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) { @@ -126,10 +188,24 @@ class Package_Manager { } elseif ( $delete_all_packages ) { $this->delete_packages_bulk(); } else { + /** + * Fired when a request action that is not handled by default is received. + * + * @param string $action The action received in the request. + * @param array|string|false $packages The packages involved in the action. + * @since 1.0.0 + */ do_action( 'upserv_udpdate_manager_request_action', $action, $packages ); } } + /** + * Admin menu hook + * + * Adds the plugin page to the admin menu. + * + * @since 1.0.0 + */ public function admin_menu() { $page_title = __( 'UpdatePulse Server', 'updatepulse-server' ); $capability = 'manage_options'; @@ -139,6 +215,13 @@ class Package_Manager { add_submenu_page( 'upserv-page', $page_title, $menu_title, $capability, 'upserv-page', $function ); } + /** + * Add page options + * + * Adds screen options for the plugin page. + * + * @since 1.0.0 + */ public function add_page_options() { $option = 'per_page'; $args = array( @@ -150,6 +233,15 @@ class Package_Manager { add_screen_option( $option, $args ); } + /** + * Enqueue admin scripts + * + * Enqueues admin scripts for the plugin page. + * + * @param array $scripts Existing scripts. + * @return array Modified scripts. + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -191,12 +283,29 @@ class Package_Manager { 'debug' => (bool) ( constant( 'WP_DEBUG' ) ), 'ajax_url' => admin_url( 'admin-ajax.php' ), ), + /** + * Filter the internationalization strings passed to the frontend scripts. + * + * @param array $l10n The internationalization strings passed to the frontend scripts. + * @param string $handle The handle of the script. + * @return array The filtered internationalization strings. + * @since 1.0.0 + */ 'l10n' => apply_filters( 'upserv_scripts_l10n', $l10n, 'package' ), ); return $scripts; } + /** + * Enqueue admin styles + * + * Enqueues admin styles for the plugin page. + * + * @param array $styles Existing styles. + * @return array Modified styles. + * @since 1.0.0 + */ public function upserv_admin_styles( $styles ) { $styles['package'] = array( 'path' => UPSERV_PLUGIN_PATH . 'css/admin/package' . upserv_assets_suffix() . '.css', @@ -206,10 +315,30 @@ class Package_Manager { return $styles; } + /** + * Set page options + * + * Sets screen options for the plugin page. + * + * @param mixed $status Screen option status. + * @param string $option Screen option name. + * @param mixed $value Screen option value. + * @return mixed Screen option value. + * @since 1.0.0 + */ public function set_page_options( $status, $option, $value ) { return $value; } + /** + * Admin tab links + * + * Adds tab links for the plugin page. + * + * @param array $links Existing tab links. + * @return array Modified tab links. + * @since 1.0.0 + */ public function upserv_admin_tab_links( $links ) { $links['main'] = array( admin_url( 'admin.php?page=upserv-page' ), @@ -219,12 +348,29 @@ class Package_Manager { return $links; } + /** + * Admin tab states + * + * Sets tab states for the plugin page. + * + * @param array $states Existing tab states. + * @param string $page Current page. + * @return array Modified tab states. + * @since 1.0.0 + */ public function upserv_admin_tab_states( $states, $page ) { $states['main'] = 'upserv-page' === $page; return $states; } + /** + * Force clean + * + * Forces a cleanup of the specified filesystem data type. + * + * @since 1.0.0 + */ public function force_clean() { $result = false; $type = false; @@ -255,10 +401,27 @@ class Package_Manager { } } + /** + * Download remote package aborted + * + * Handles the event when a remote package download is aborted. + * + * @param string $safe_slug Safe slug of the package. + * @param string $type Type of the package. + * @param array $info Additional information. + * @since 1.0.0 + */ public function upserv_download_remote_package_aborted( $safe_slug, $type, $info ) { wp_cache_set( 'upserv_download_remote_package_aborted', $info, 'updatepulse-server' ); } + /** + * Register package from VCS + * + * Registers a package from a Version Control System (VCS). + * + * @since 1.0.0 + */ public function register_package_from_vcs() { $result = false; $error = false; @@ -310,6 +473,13 @@ class Package_Manager { wp_cache_delete( 'upserv_download_remote_package_aborted', 'updatepulse-server' ); } + /** + * Fired after a package has been registered from a VCS. + * + * @param bool $result Whether the package was successfully registered. + * @param string $slug The slug of the registered package. + * @since 1.0.0 + */ do_action( 'upserv_registered_package_from_vcs', $result, $slug ); if ( ! $error && $result ) { @@ -327,6 +497,13 @@ class Package_Manager { } } + /** + * Manual package upload + * + * Handles manual package uploads. + * + * @since 1.0.0 + */ public function manual_package_upload() { $result = false; $slug = 'N/A'; @@ -401,6 +578,14 @@ class Package_Manager { $sanitized_part( $files['package']['tmp_name'] ) !== $files['package']['tmp_name'] || sanitize_file_name( $files['package']['name'] ) !== $files['package']['name'] ) { + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); wp_send_json_error( new WP_Error( @@ -421,6 +606,14 @@ class Package_Manager { if ( ! in_array( $files['package']['type'], $valid_archive_formats, true ) ) { $wp_filesystem->delete( $files['package']['tmp_name'] ); + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); wp_send_json_error( new WP_Error( @@ -470,6 +663,14 @@ class Package_Manager { } $wp_filesystem->delete( $files['package']['tmp_name'] ); + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); wp_send_json_error( new WP_Error( @@ -481,6 +682,14 @@ class Package_Manager { if ( 0 >= $files['package']['size'] ) { $wp_filesystem->delete( $files['package']['tmp_name'] ); + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); wp_send_json_error( new WP_Error( @@ -496,6 +705,14 @@ class Package_Manager { if ( ! $parsed_info ) { $wp_filesystem->delete( $files['package']['tmp_name'] ); + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); wp_send_json_error( new WP_Error( @@ -513,6 +730,14 @@ class Package_Manager { $dest = Data_Manager::get_data_dir( 'packages' ) . $filename; $result = $wp_filesystem->move( $files['package']['tmp_name'], $dest, true ); + /** + * Fired after an attempt to manually upload a package. + * + * @param bool $result Whether the upload was successful. + * @param string $type The package type. + * @param string $slug The package slug. + * @since 1.0.0 + */ do_action( 'upserv_did_manual_upload_package', $result, $type, $slug ); if ( $result ) { @@ -536,12 +761,28 @@ class Package_Manager { } } + /** + * Pre delete package hook + * + * Handles tasks before a package is deleted. + * + * @param string $package_slug Package slug. + * @since 1.0.0 + */ public function upserv_package_manager_pre_delete_package( $package_slug ) { $info = upserv_get_package_info( $package_slug, false ); wp_cache_set( 'upserv_package_manager_pre_delete_package_info', $info, 'updatepulse-server' ); } + /** + * Package deleted hook + * + * Handles tasks after a package is deleted. + * + * @param string $package_slug Package slug. + * @since 1.0.0 + */ public function upserv_package_manager_deleted_package( $package_slug ) { $package_info = wp_cache_get( 'upserv_package_manager_pre_delete_package_info', 'updatepulse-server' ); @@ -559,6 +800,14 @@ class Package_Manager { // Misc. ------------------------------------------------------- + /** + * Get instance + * + * Returns the singleton instance of the class. + * + * @return Package_Manager + * @since 1.0.0 + */ public static function get_instance() { if ( ! self::$instance ) { @@ -568,6 +817,13 @@ class Package_Manager { return self::$instance; } + /** + * Plugin page + * + * Renders the plugin page. + * + * @since 1.0.0 + */ public function plugin_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -619,13 +875,31 @@ class Package_Manager { ); } + /** + * Delete packages in bulk + * + * Deletes multiple packages in bulk. + * + * @param array $package_slugs Package slugs to delete. + * @return array|false Deleted package slugs or false if no packages were deleted. + * @since 1.0.0 + */ public function delete_packages_bulk( $package_slugs = array() ) { $package_directory = Data_Manager::get_data_dir( 'packages' ); $package_paths = glob( trailingslashit( $package_directory ) . '*.zip' ); $package_names = array(); $deleted_package_slugs = array(); $delete_all = false; - $package_paths = apply_filters( + + /** + * Filter the package paths to be deleted in bulk. + * + * @param array $package_paths Paths to package files. + * @param array $package_slugs Package slugs to delete. + * @return array The filtered package paths. + * @since 1.0.0 + */ + $package_paths = apply_filters( 'upserv_delete_packages_bulk_paths', $package_paths, $package_slugs @@ -649,16 +923,32 @@ class Package_Manager { return; } - $url = home_url( '/updatepulse-server-update-api/' ); - $filter_args = array( - 'url' => $url, - ); - $_class_name = apply_filters( + $url = home_url( '/updatepulse-server-update-api/' ); + $filter_args = array( 'url' => $url ); + /** + * Filter the class name used for the update server. + * + * @param string $class_name The default class name. + * @param mixed $null Null value. + * @param array $filter_args Arguments for the filter. + * @return string The filtered class name. + * @since 1.0.0 + */ + $_class_name = apply_filters( 'upserv_server_class_name', str_replace( 'Manager', 'Server\\Update', __NAMESPACE__ ) . '\\Update_Server', null, $filter_args ); + /** + * Filter the constructor arguments for the update server. + * + * @param array $args Constructor arguments. + * @param mixed $null Null value. + * @param array $filter_args Arguments for the filter. + * @return array The filtered constructor arguments. + * @since 1.0.0 + */ $args = apply_filters( 'upserv_server_constructor_args', array( $url, Data_Manager::get_data_dir(), '', '', '', '', false ), @@ -667,16 +957,35 @@ class Package_Manager { ); $update_server = new $_class_name( ...$args ); + /** + * Fired before deleting multiple packages in bulk. + * + * @param array $package_slugs Slugs of packages about to be deleted. + * @since 1.0.0 + */ do_action( 'upserv_package_manager_pre_delete_packages_bulk', $package_slugs ); foreach ( $package_slugs as $slug ) { $package_name = $slug . '.zip'; if ( in_array( $package_name, $package_names, true ) ) { + /** + * Fired before a package is deleted. + * + * @param string $slug The slug of the package to be deleted. + * @since 1.0.0 + */ do_action( 'upserv_package_manager_pre_delete_package', $slug ); $result = $update_server->remove_package( $slug ); + /** + * Fired after a package has been deleted. + * + * @param string $slug The slug of the deleted package. + * @param bool $result Whether the package was successfully deleted. + * @since 1.0.0 + */ do_action( 'upserv_package_manager_deleted_package', $slug, $result ); if ( $result ) { @@ -690,12 +999,27 @@ class Package_Manager { } if ( ! empty( $deleted_package_slugs ) ) { + /** + * Fired after multiple packages have been deleted in bulk. + * + * @param array $deleted_package_slugs Slugs of packages that were deleted. + * @since 1.0.0 + */ do_action( 'upserv_package_manager_deleted_packages_bulk', $deleted_package_slugs ); } return empty( $deleted_package_slugs ) ? false : $deleted_package_slugs; } + /** + * Download packages in bulk + * + * Downloads multiple packages in bulk. + * + * @param array $package_slugs Package slugs to download. + * @return void + * @since 1.0.0 + */ public function download_packages_bulk( $package_slugs ) { WP_Filesystem(); @@ -713,6 +1037,14 @@ class Package_Manager { $archive_name = reset( $package_slugs ); $archive_path = trailingslashit( $package_directory ) . $archive_name . '.zip'; + /** + * Fired before packages are downloaded. + * + * @param string $archive_name The name of the archive. + * @param string $archive_path The path to the archive. + * @param array $package_slugs The slugs of the packages to download. + * @since 1.0.0 + */ do_action( 'upserv_before_packages_download', $archive_name, $archive_path, $package_slugs ); foreach ( $package_slugs as $package_slug ) { @@ -734,6 +1066,14 @@ class Package_Manager { $archive_name = 'archive-' . time(); $archive_path = trailingslashit( $temp_directory ) . $archive_name . '.zip'; + /** + * Fired before packages are repackaged for download. + * + * @param string $archive_name The name of the archive. + * @param string $archive_path The path to the archive. + * @param array $package_slugs The slugs of the packages to repackage. + * @since 1.0.0 + */ do_action( 'upserv_before_packages_download_repack', $archive_name, $archive_path, $package_slugs ); foreach ( $package_slugs as $package_slug ) { @@ -762,10 +1102,28 @@ class Package_Manager { $zip->close(); + /** + * Fired before packages are downloaded. + * + * @param string $archive_name The name of the archive. + * @param string $archive_path The path to the archive. + * @param array $package_slugs The slugs of the packages to download. + * @since 1.0.0 + */ do_action( 'upserv_before_packages_download', $archive_name, $archive_path, $package_slugs ); $this->trigger_packages_download( $archive_name, $archive_path ); } + /** + * Trigger packages download + * + * Triggers the download of the specified archive. + * + * @param string $archive_name Archive name. + * @param string $archive_path Archive path. + * @param boolean $exit_or_die Whether to exit or die after download. + * @since 1.0.0 + */ public function trigger_packages_download( $archive_name, $archive_path, $exit_or_die = true ) { if ( ! empty( $archive_path ) && is_file( $archive_path ) && ! empty( $archive_name ) ) { @@ -848,11 +1206,25 @@ class Package_Manager { header( 'Content-Transfer-Encoding: binary' ); header( 'Content-Length: ' . filesize( $archive_path ) ); + /** + * Fired when a packages download is triggered. + * + * @param string $archive_name The name of the archive. + * @param string $archive_path The path to the archive. + * @since 1.0.0 + */ do_action( 'upserv_triggered_packages_download', $archive_name, $archive_path ); echo @file_get_contents( $archive_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * Fired after packages have been downloaded. + * + * @param string $archive_name The name of the archive. + * @param string $archive_path The path to the archive. + * @since 1.0.0 + */ do_action( 'upserv_after_packages_download', $archive_name, $archive_path ); if ( $exit_or_die ) { @@ -860,6 +1232,15 @@ class Package_Manager { } } + /** + * Get package info + * + * Retrieves information about a package. + * + * @param string $slug Package slug. + * @return array|false Package information or false if not found. + * @since 1.0.0 + */ public function get_package_info( $slug ) { $package_info = wp_cache_get( 'package_info_' . $slug, 'updatepulse-server' ); @@ -867,9 +1248,24 @@ class Package_Manager { return $package_info; } + /** + * Fired when retrieving package information. + * + * @param mixed $package_info Package information or false. + * @param string $slug Package slug. + * @since 1.0.0 + */ do_action( 'upserv_get_package_info', $package_info, $slug ); if ( has_filter( 'upserv_package_manager_get_package_info' ) ) { + /** + * Filter the package information for a specific package. + * + * @param mixed $package_info Package information or false. + * @param string $slug Package slug. + * @return array The filtered package information. + * @since 1.0.0 + */ $package_info = apply_filters( 'upserv_package_manager_get_package_info', $package_info, $slug ); } else { $package_directory = Data_Manager::get_data_dir( 'packages' ); @@ -910,11 +1306,28 @@ class Package_Manager { $package_info['metadata'] = $this->get_package_metadata( $slug ); } + /** + * Filter the package information before returning it. + * + * @param array $package_info Package information. + * @param string $slug Package slug. + * @return array The filtered package information. + * @since 1.0.0 + */ $package_info = apply_filters( 'upserv_package_manager_package_info', $package_info, $slug ); return $package_info; } + /** + * Get batch package info + * + * Retrieves information about multiple packages. + * + * @param string|false $search Search term. + * @return array Package information. + * @since 1.0.0 + */ public function get_batch_package_info( $search = false ) { $packages = wp_cache_get( 'packages', 'updatepulse-server' ); @@ -923,6 +1336,14 @@ class Package_Manager { } if ( has_filter( 'upserv_package_manager_get_batch_package_info' ) ) { + /** + * Filter the batch package information. + * + * @param mixed $packages Package information or false. + * @param string|false $search Search term. + * @return array The filtered batch package information. + * @since 1.0.0 + */ $packages = apply_filters( 'upserv_package_manager_get_batch_package_info', $packages, $search ); wp_cache_set( 'packages', $packages, 'updatepulse-server' ); @@ -966,6 +1387,15 @@ class Package_Manager { false === strpos( strtolower( $meta['slug'] ) . '.zip', strtolower( $search ) ) ) ); + + /** + * Filter whether to include a package in the batch info results. + * + * @param bool $include Whether to include the package. + * @param array $meta Package metadata. + * @return bool The filtered inclusion decision. + * @since 1.0.0 + */ $include = apply_filters( 'upserv_package_info_include', $include, $meta ); if ( ! $include ) { @@ -993,6 +1423,14 @@ class Package_Manager { } } + /** + * Filter the batch package information before returning it. + * + * @param array $packages Package information. + * @param string|false $search Search term. + * @return array The filtered batch package information. + * @since 1.0.0 + */ $packages = apply_filters( 'upserv_package_manager_batch_package_info', $packages, $search ); wp_cache_set( 'packages', $packages, 'updatepulse-server' ); @@ -1004,9 +1442,26 @@ class Package_Manager { return $packages; } + /** + * Check if package is whitelisted + * + * Checks if a package is whitelisted. + * + * @param string $package_slug Package slug. + * @return boolean True if whitelisted, false otherwise. + * @since 1.0.0 + */ public function is_package_whitelisted( $package_slug ) { if ( has_filter( 'upserv_is_package_whitelisted' ) ) { + /** + * Filter whether a package is whitelisted. + * + * @param bool $is_whitelisted Whether the package is whitelisted. + * @param string $package_slug Package slug. + * @return bool The filtered whitelisted status. + * @since 1.0.0 + */ return apply_filters( 'upserv_is_package_whitelisted', false, $package_slug ); } @@ -1025,6 +1480,15 @@ class Package_Manager { return false; } + /** + * Whitelist package + * + * Whitelists a package. + * + * @param string $package_slug Package slug. + * @return boolean True if successful, false otherwise. + * @since 1.0.0 + */ public function whitelist_package( $package_slug ) { $data = $this->get_package_metadata( $package_slug, false ); @@ -1033,6 +1497,14 @@ class Package_Manager { } if ( has_filter( 'upserv_whitelist_package_data' ) ) { + /** + * Filter the data used to whitelist a package. + * + * @param array $data Package metadata. + * @param string $package_slug Package slug. + * @return array The filtered package metadata. + * @since 1.0.0 + */ $data = apply_filters( 'upserv_whitelist_package_data', $data, $package_slug ); } else { $data['whitelisted']['local'] = array( true, time() ); @@ -1040,11 +1512,28 @@ class Package_Manager { $result = $this->set_package_metadata( $package_slug, $data ); + /** + * Fired after a package has been whitelisted. + * + * @param string $package_slug Package slug. + * @param array $data Package metadata. + * @param bool $result Whether the metadata was successfully updated. + * @since 1.0.0 + */ do_action( 'upserv_whitelist_package', $package_slug, $data, $result ); return $result; } + /** + * Unwhitelist package + * + * Unwhitelists a package. + * + * @param string $package_slug Package slug. + * @return boolean True if successful, false otherwise. + * @since 1.0.0 + */ public function unwhitelist_package( $package_slug ) { $data = $this->get_package_metadata( $package_slug, false ); @@ -1053,6 +1542,14 @@ class Package_Manager { } if ( has_filter( 'upserv_unwhitelist_package_data' ) ) { + /** + * Filter the data used to unwhitelist a package. + * + * @param array $data Package metadata. + * @param string $package_slug Package slug. + * @return array The filtered package metadata. + * @since 1.0.0 + */ $data = apply_filters( 'upserv_unwhitelist_package_data', $data, $package_slug ); } else { $data['whitelisted']['local'] = array( false, time() ); @@ -1060,11 +1557,28 @@ class Package_Manager { $result = $this->set_package_metadata( $package_slug, $data ); + /** + * Fired after a package has been unwhitelisted. + * + * @param string $package_slug Package slug. + * @param bool $result Whether the metadata was successfully updated. + * @since 1.0.0 + */ do_action( 'upserv_unwhitelist_package', $package_slug, $result ); return $result; } + /** + * Get package metadata + * + * Retrieves metadata for a package. + * + * @param string $package_slug Package slug. + * @param boolean $json_encode Whether to return JSON encoded data. + * @return array|string Package metadata. + * @since 1.0.0 + */ public function get_package_metadata( $package_slug, $json_encode = false ) { $data = wp_cache_get( 'package_metadata_' . $package_slug, 'updatepulse-server' ); @@ -1080,6 +1594,15 @@ class Package_Manager { if ( ! has_filter( 'upserv_get_package_metadata' ) && is_file( $file_path ) ) { $data = @file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.NoSilencedErrors.Discouraged } elseif ( has_filter( 'upserv_get_package_metadata' ) ) { + /** + * Filter the package metadata for a specific package. + * + * @param string $data JSON encoded package metadata. + * @param string $package_slug Package slug. + * @param bool $json_encode Whether to return JSON encoded data. + * @return string The filtered package metadata. + * @since 1.0.0 + */ $data = apply_filters( 'upserv_get_package_metadata', $data, $package_slug, $json_encode ); } @@ -1092,6 +1615,16 @@ class Package_Manager { return $data; } + /** + * Set package metadata + * + * Sets metadata for a package. + * + * @param string $package_slug Package slug. + * @param array $metadata Package metadata. + * @return boolean True if successful, false otherwise. + * @since 1.0.0 + */ public function set_package_metadata( $package_slug, $metadata ) { WP_Filesystem(); @@ -1101,7 +1634,16 @@ class Package_Manager { $filename = sanitize_file_name( $package_slug . '.json' ); $file_path = trailingslashit( $dir ) . $filename; $result = false; - $data = apply_filters( 'upserv_set_package_metadata_data', $metadata, $package_slug ); + + /** + * Filter the metadata to be set for a package. + * + * @param array $metadata Package metadata. + * @param string $package_slug Package slug. + * @return array The filtered package metadata. + * @since 1.0.0 + */ + $data = apply_filters( 'upserv_set_package_metadata_data', $metadata, $package_slug ); wp_cache_delete( 'package_metadata_' . $package_slug, 'updatepulse-server' ); @@ -1110,9 +1652,24 @@ class Package_Manager { if ( ! has_filter( 'upserv_did_delete_package_metadata' ) && is_file( $file_path ) ) { $result = (bool) $wp_filesystem->delete( $file_path ); } else { + /** + * Filter the result of deleting package metadata. + * + * @param bool $result Whether deletion was successful. + * @param string $package_slug Package slug. + * @return bool The filtered result. + * @since 1.0.0 + */ $result = apply_filters( 'upserv_did_delete_package_metadata', false, $package_slug ); } + /** + * Fired after package metadata has been deleted. + * + * @param string $package_slug Package slug. + * @param bool $result Whether the metadata was successfully deleted. + * @since 1.0.0 + */ do_action( 'upserv_delete_package_metadata', $package_slug, $result ); return $result; @@ -1133,9 +1690,26 @@ class Package_Manager { FS_CHMOD_FILE ); } else { + /** + * Filter the result of setting package metadata. + * + * @param bool $result Whether the operation was successful. + * @param string $package_slug Package slug. + * @param array $data Package metadata. + * @return bool The filtered result. + * @since 1.0.0 + */ $result = apply_filters( 'upserv_did_set_package_metadata', false, $package_slug, $data ); } + /** + * Fired after package metadata has been set. + * + * @param string $package_slug Package slug. + * @param array $data Package metadata. + * @param bool $result Whether the metadata was successfully updated. + * @since 1.0.0 + */ do_action( 'upserv_set_package_metadata', $package_slug, $data, $result ); return $result; @@ -1145,6 +1719,15 @@ class Package_Manager { * Protected methods *******************************************************************/ + /** + * Get directory size in MB + * + * Retrieves the size of the specified directory in MB. + * + * @param string $type Directory type. + * @return string Directory size in MB. + * @since 1.0.0 + */ protected static function get_dir_size_mb( $type ) { $result = 'N/A'; @@ -1174,6 +1757,14 @@ class Package_Manager { return $result; } + /** + * Plugin options handler + * + * Handles the submission of plugin options. + * + * @return array|string Result of the options update. + * @since 1.0.0 + */ protected function plugin_options_handler() { $errors = array(); $result = ''; @@ -1198,6 +1789,16 @@ class Package_Manager { $condition = is_numeric( $option_info['value'] ); } + /** + * Filter whether to update a package option. + * + * @param bool $condition Whether the option should be updated. + * @param string $option_name Option name. + * @param array $option_info Option information. + * @param array $options All submitted options. + * @return bool The filtered update condition. + * @since 1.0.0 + */ $condition = apply_filters( 'upserv_package_option_update', $condition, @@ -1232,13 +1833,34 @@ class Package_Manager { $result = $errors; } + /** + * Fired after package options have been updated. + * + * @param array|string $result The result of the update operation. + * @since 1.0.0 + */ do_action( 'upserv_package_options_updated', $result ); return $result; } + /** + * Get submitted options + * + * Retrieves the submitted options from the form. + * + * @return array Submitted options. + * @since 1.0.0 + */ protected function get_submitted_options() { + /** + * Filter the submitted package configuration options. + * + * @param array $config Default package configuration. + * @return array The filtered package configuration. + * @since 1.0.0 + */ return apply_filters( 'upserv_submitted_package_config', array( @@ -1267,6 +1889,16 @@ class Package_Manager { ); } + /** + * Get package + * + * Retrieves a package object from the specified file. + * + * @param string $filename Package file name. + * @param string $slug Package slug. + * @return Package|false Package object or false if not found. + * @since 1.0.0 + */ protected function get_package( $filename, $slug ) { $package = false; $cache = new Cache( Data_Manager::get_data_dir( 'cache' ) ); @@ -1280,6 +1912,14 @@ class Package_Manager { } if ( null === $cached_value ) { + /** + * Fired when a package is not found in the cache. + * + * @param string $slug Package slug. + * @param string $filename Path to the package file. + * @param Cache $cache Cache object. + * @since 1.0.0 + */ do_action( 'upserv_find_package_no_cache', $slug, $filename, $cache ); } diff --git a/inc/manager/class-remote-sources-manager.php b/inc/manager/class-remote-sources-manager.php index 7bdcdb2..bd93012 100644 --- a/inc/manager/class-remote-sources-manager.php +++ b/inc/manager/class-remote-sources-manager.php @@ -11,8 +11,19 @@ use Anyape\UpdatePulse\Server\Manager\Data_Manager; use Anyape\UpdatePulse\Server\API\Update_API; use Anyape\UpdatePulse\Server\Scheduler\Scheduler; +/** + * Remote Sources Manager class + * + * @since 1.0.0 + */ class Remote_Sources_Manager { + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize WordPress hooks. + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -40,14 +51,35 @@ class Remote_Sources_Manager { // WordPress hooks --------------------------------------------- + /** + * Activate + * + * Register schedules when the plugin is activated. + * + * @since 1.0.0 + */ public static function activate() { self::register_schedules(); } + /** + * Deactivate + * + * Clear schedules when the plugin is deactivated. + * + * @since 1.0.0 + */ public static function deactivate() { self::clear_schedules(); } + /** + * Enqueue admin scripts + * + * @param array $scripts List of scripts to enqueue. + * @return array Modified list of scripts. + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $page = ! empty( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -64,6 +96,13 @@ class Remote_Sources_Manager { return $scripts; } + /** + * Enqueue admin styles + * + * @param array $styles List of styles to enqueue. + * @return array Modified list of styles. + * @since 1.0.0 + */ public function upserv_admin_styles( $styles ) { $styles['remote_sources'] = array( 'path' => UPSERV_PLUGIN_PATH . 'css/admin/remote-sources' . upserv_assets_suffix() . '.css', @@ -73,6 +112,11 @@ class Remote_Sources_Manager { return $styles; } + /** + * Register remote check scheduled hooks + * + * @since 1.0.0 + */ public function register_remote_check_scheduled_hooks() { if ( upserv_is_doing_update_api_request() ) { @@ -104,6 +148,15 @@ class Remote_Sources_Manager { foreach ( $slugs as $slug ) { add_action( 'upserv_check_remote_' . $slug, $action_hook, 10, 3 ); + + /** + * Fired after a remote check action has been registered for a package. + * Fired during client update API request. + * + * @param string $package_slug The slug of the package for which an action has been registered + * @param string $scheduled_hook The event hook the action has been registered to + * @param string $action_hook The action that has been registered + */ do_action( 'upserv_registered_check_remote_schedule', $slug, @@ -114,6 +167,13 @@ class Remote_Sources_Manager { } } + /** + * Clear remote check scheduled hooks + * + * @param array|null $vcs_configs VCS configurations. + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public function clear_remote_check_scheduled_hooks( $vcs_configs = null ) { if ( upserv_is_doing_update_api_request() ) { @@ -144,6 +204,14 @@ class Remote_Sources_Manager { $scheduled_hook = 'upserv_check_remote_' . $slug; Scheduler::get_instance()->unschedule_all_actions( $scheduled_hook ); + + /** + * Fired after a remote check schedule event has been unscheduled for a package. + * Fired during client update API request. + * + * @param string $package_slug The slug of the package for which a remote check event has been unscheduled + * @param string $scheduled_hook The remote check event hook that has been unscheduled + */ do_action( 'upserv_cleared_check_remote_schedule', $slug, $scheduled_hook ); } } @@ -151,6 +219,11 @@ class Remote_Sources_Manager { return true; } + /** + * Add admin menu + * + * @since 1.0.0 + */ public function admin_menu() { $function = array( $this, 'plugin_page' ); $page_title = __( 'UpdatePulse Server - Version Control Systems ', 'updatepulse-server' ); @@ -160,6 +233,13 @@ class Remote_Sources_Manager { add_submenu_page( 'upserv-page', $page_title, $menu_title, 'manage_options', $menu_slug, $function ); } + /** + * Add admin tab links + * + * @param array $links List of admin tab links. + * @return array Modified list of admin tab links. + * @since 1.0.0 + */ public function upserv_admin_tab_links( $links ) { $links['remote-sources'] = array( admin_url( 'admin.php?page=upserv-page-remote-sources' ), @@ -169,12 +249,25 @@ class Remote_Sources_Manager { return $links; } + /** + * Add admin tab states + * + * @param array $states List of admin tab states. + * @param string $page Current admin page. + * @return array Modified list of admin tab states. + * @since 1.0.0 + */ public function upserv_admin_tab_states( $states, $page ) { $states['remote-sources'] = 'upserv-page-remote-sources' === $page; return $states; } + /** + * Force clean + * + * @since 1.0.0 + */ public function force_clean() { $result = false; $type = false; @@ -225,6 +318,11 @@ class Remote_Sources_Manager { } } + /** + * VCS test + * + * @since 1.0.0 + */ public function vcs_test() { $result = false; @@ -301,12 +399,23 @@ class Remote_Sources_Manager { // Misc. ------------------------------------------------------- + /** + * Clear schedules + * + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public static function clear_schedules() { $manager = new self(); return $manager->clear_remote_check_scheduled_hooks(); } + /** + * Register schedules + * + * @since 1.0.0 + */ public static function register_schedules() { $options = get_option( 'upserv_options' ); $options = json_decode( $options, true ); @@ -329,6 +438,13 @@ class Remote_Sources_Manager { } } + /** + * Reschedule remote check recurring events + * + * @param array $vcs_c VCS configuration. + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public function reschedule_remote_check_recurring_events( $vcs_c ) { if ( @@ -346,10 +462,17 @@ class Remote_Sources_Manager { } foreach ( $slugs as $slug ) { - $meta = upserv_get_package_metadata( $slug ); - $type = isset( $meta['type'] ) ? $meta['type'] : null; - $hook = 'upserv_check_remote_' . $slug; - $params = array( $slug, $type, false ); + $meta = upserv_get_package_metadata( $slug ); + $type = isset( $meta['type'] ) ? $meta['type'] : null; + $hook = 'upserv_check_remote_' . $slug; + $params = array( $slug, $type, false ); + + /** + * Filter the frequency at which remote checks for updates are performed for a package. + * + * @param string $frequency The frequency at which remote checks are performed + * @param string $package_slug The slug of the package + */ $frequency = apply_filters( 'upserv_check_remote_frequency', isset( $vcs_c['check_frequency'] ) ? $vcs_c['check_frequency'] : 'daily', @@ -359,6 +482,14 @@ class Remote_Sources_Manager { $schedules = wp_get_schedules(); Scheduler::get_instance()->unschedule_all_actions( $hook, $params ); + + /** + * Fired after a remote check schedule event has been unscheduled for a package. + * Fired during client update API request. + * + * @param string $package_slug The slug of the package for which a remote check event has been unscheduled + * @param string $scheduled_hook The remote check event hook that has been unscheduled + */ do_action( 'upserv_cleared_check_remote_schedule', $slug, $hook ); $result = Scheduler::get_instance()->schedule_recurring_action( @@ -368,6 +499,17 @@ class Remote_Sources_Manager { $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 The 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 + */ do_action( 'upserv_scheduled_check_remote_event', $result, @@ -382,6 +524,11 @@ class Remote_Sources_Manager { return true; } + /** + * Plugin page + * + * @since 1.0.0 + */ public function plugin_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -419,6 +566,12 @@ class Remote_Sources_Manager { * Protected methods *******************************************************************/ + /** + * Plugin options handler + * + * @return array|string Result of the options update. + * @since 1.0.0 + */ protected function plugin_options_handler() { $errors = array(); $result = ''; @@ -454,6 +607,14 @@ class Remote_Sources_Manager { $option_info['value'] = (bool) $option_info['value']; } + /** + * Filter whether to update the remote source option. + * + * @param bool $condition Whether to update the option + * @param string $option_name The name of the option + * @param array $option_info Information about the option + * @param array $options All submitted options + */ $condition = apply_filters( 'upserv_remote_source_option_update', $condition, @@ -463,6 +624,14 @@ class Remote_Sources_Manager { ); if ( $condition ) { + /** + * Filter the value of the remote source option before saving it. + * + * @param mixed $value The value of the option + * @param string $option_name The name of the option + * @param array $option_info Information about the option + * @param array $options All submitted options + */ $to_save[ $option_info['path'] ] = apply_filters( 'upserv_remote_sources_option_save_value', $option_info['value'], @@ -567,11 +736,26 @@ class Remote_Sources_Manager { } set_transient( 'upserv_flush', 1, 60 ); + + /** + * Fired after the options in "Remote Sources" have been updated. + * + * @param array|string $result The result of the options update, an array of errors or a success message + */ do_action( 'upserv_remote_sources_options_updated', $result ); return $result; } + /** + * Filter JSON input + * + * @param array $inputs JSON input data. + * @param string $option_name Option name. + * @param array $errors List of errors. + * @return array Filtered JSON input data. + * @since 1.0.0 + */ protected function filter_json_input( $inputs, $option_name, &$errors ) { $filtered = array(); $index = 0; @@ -642,7 +826,19 @@ class Remote_Sources_Manager { return $filtered; } + /** + * Get submitted options + * + * @return array List of submitted options. + * @since 1.0.0 + */ protected function get_submitted_options() { + /** + * Filter the submitted remote sources configuration values before using them. + * + * @param array $config The submitted remote sources configuration values + * @return array The filtered configuration + */ return apply_filters( 'upserv_submitted_remote_sources_config', array( @@ -663,6 +859,13 @@ class Remote_Sources_Manager { ); } + /** + * Get package slugs + * + * @param string $vcs_url VCS URL. + * @return array List of package slugs. + * @since 1.0.0 + */ protected function get_package_slugs( $vcs_url ) { $slugs = wp_cache_get( 'package_slugs', 'updatepulse-server' ); diff --git a/inc/manager/class-zip-package-manager.php b/inc/manager/class-zip-package-manager.php index b4d6140..c3d5b0d 100644 --- a/inc/manager/class-zip-package-manager.php +++ b/inc/manager/class-zip-package-manager.php @@ -6,19 +6,61 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } +use WP_Error; use ZipArchive; use RecursiveIteratorIterator; use RecursiveDirectoryIterator; use FilesystemIterator; use Anyape\Utils\Utils; +/** + * Zip Package Manager class + * + * @since 1.0.0 + */ class Zip_Package_Manager { + /** + * Package slug + * + * @var string + * @since 1.0.0 + */ protected $package_slug; + + /** + * Path to the received package + * + * @var string + * @since 1.0.0 + */ protected $received_package_path; + + /** + * Temporary directory path + * + * @var string + * @since 1.0.0 + */ protected $tmp_dir; + + /** + * Packages directory path + * + * @var string + * @since 1.0.0 + */ protected $packages_dir; + /** + * Constructor + * + * @param string $package_slug The package slug. + * @param string $received_package_path Path to the received package. + * @param string $tmp_dir Temporary directory path. + * @param string $packages_dir Packages directory path. + * @since 1.0.0 + */ public function __construct( $package_slug, $received_package_path, $tmp_dir, $packages_dir ) { $this->package_slug = $package_slug; $this->received_package_path = $received_package_path; @@ -30,10 +72,31 @@ class Zip_Package_Manager { * Public methods *******************************************************************/ + /** + * Unzip package + * + * Extract a zip package to a destination. + * + * @param string $source Path to the source zip file. + * @param string $destination Path to the destination directory. + * @return bool|WP_Error True on success, WP_Error on failure. + * @since 1.0.0 + */ public static function unzip_package( $source, $destination ) { return unzip_file( $source, $destination ); } + /** + * Zip package + * + * Create a zip archive from a directory or file. + * + * @param string $source Path to the source directory or file. + * @param string $destination Path to the destination zip file. + * @param string $container_dir Optional container directory within the zip. + * @return bool Whether the zip creation was successful. + * @since 1.0.0 + */ public static function zip_package( $source, $destination, $container_dir = '' ) { $zip = new ZipArchive(); @@ -86,6 +149,14 @@ class Zip_Package_Manager { return $zip->close() && file_exists( $destination ); } + /** + * Clean package + * + * Clean the received package by moving and repacking it. + * + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public function clean_package() { WP_Filesystem(); @@ -94,7 +165,7 @@ class Zip_Package_Manager { $return = true; $error_message = __METHOD__ . ': '; - if ( is_wp_error( $this->received_package_path ) ) { + if ( $this->received_package_path instanceof WP_Error ) { $return = false; $error_message .= $this->received_package_path->get_error_message(); } @@ -169,6 +240,14 @@ class Zip_Package_Manager { * Protected methods *******************************************************************/ + /** + * Repack package + * + * Repack the received package by unzipping and zipping it again. + * + * @return bool True on success, false on failure. + * @since 1.0.0 + */ protected function repack_package() { WP_Filesystem(); @@ -207,6 +286,14 @@ class Zip_Package_Manager { $wp_filesystem->chmod( $temp_path, false, true ); + /** + * Fired before packing the files received from the Version Control System. Can be used for extra files manipulation. + * Fired during client update API request. + * + * @param string $package_slug The slug of the package. + * @param string $files_path The path of the directory where the package files are located. + * @param string $archive_path The path where the package archive will be located after packing. + */ do_action( 'upserv_before_remote_package_zip', $this->package_slug, $temp_path, $archive_path ); $zipped = self::zip_package( $temp_path, $archive_path ); diff --git a/inc/nonce/class-nonce.php b/inc/nonce/class-nonce.php index f639593..2bce8a5 100644 --- a/inc/nonce/class-nonce.php +++ b/inc/nonce/class-nonce.php @@ -12,25 +12,91 @@ use PasswordHash; use Anyape\Utils\Utils; use Anyape\UpdatePulse\Server\Scheduler\Scheduler; +/** + * Nonce class + * + * @since 1.0.0 + */ class Nonce { + /** + * Default expiry length + * + * Default time in seconds before a nonce expires. + * + * @var int + * @since 1.0.0 + */ const DEFAULT_EXPIRY_LENGTH = MINUTE_IN_SECONDS / 2; - const NONCE_ONLY = 1; - const NONCE_INFO_ARRAY = 2; + /** + * Nonce only return type + * + * Constant indicating to return just the nonce string. + * + * @var int + * @since 1.0.0 + */ + const NONCE_ONLY = 1; + /** + * Nonce info array return type + * + * Constant indicating to return the nonce with additional information. + * + * @var int + * @since 1.0.0 + */ + const NONCE_INFO_ARRAY = 2; + /** + * True nonce flag + * + * Indicates if a nonce is a true nonce. + * + * @var bool|null + * @since 1.0.0 + */ protected static $true_nonce; + /** + * Expiry length + * + * Time in seconds before a nonce expires. + * + * @var int|null + * @since 1.0.0 + */ protected static $expiry_length; + /** + * API request flag + * + * Indicates if the current request is an API request. + * + * @var bool|null + * @since 1.0.0 + */ protected static $doing_api_request = null; + /** + * Private keys + * + * Array of private keys used for authentication. + * + * @var array|null + * @since 1.0.0 + */ protected static $private_keys; /******************************************************************* * Public methods *******************************************************************/ - // API action -------------------------------------------------- - // WordPress hooks --------------------------------------------- + /** + * Activate + * + * Setup necessary database tables on plugin activation. + * + * @since 1.0.0 + */ public static function activate() { $result = self::maybe_create_or_upgrade_db(); @@ -41,12 +107,33 @@ class Nonce { } } + /** + * Deactivate + * + * Clean up scheduled actions on plugin deactivation. + * + * @since 1.0.0 + */ public static function deactivate() { Scheduler::get_instance()->unschedule_all_actions( 'upserv_nonce_cleanup' ); } + /** + * Uninstall + * + * Placeholder for uninstall logic. + * + * @since 1.0.0 + */ public static function uninstall() {} + /** + * Initialize scheduler + * + * Schedule recurring actions for nonce cleanup. + * + * @since 1.0.0 + */ public static function upserv_scheduler_init() { if ( Scheduler::get_instance()->has_scheduled_action( 'upserv_nonce_cleanup' ) ) { @@ -63,6 +150,13 @@ class Nonce { ); } + /** + * Add endpoints + * + * Add rewrite rules for nonce and token endpoints. + * + * @since 1.0.0 + */ public static function add_endpoints() { add_rewrite_rule( '^updatepulse-server-token/*?$', @@ -76,6 +170,13 @@ class Nonce { ); } + /** + * Parse request + * + * Handle incoming requests to the nonce and token endpoints. + * + * @since 1.0.0 + */ public static function parse_request() { global $wp; @@ -101,6 +202,12 @@ class Nonce { unset( $payload['action'] ); + /** + * Filter the payload sent to the Nonce API. + * + * @param array $payload The payload sent to the Nonce API + * @param string $method The api action - `token` or `nonce` + */ $payload = apply_filters( 'upserv_nonce_api_payload', $payload, $method ); if ( @@ -128,12 +235,35 @@ class Nonce { } } - $code = apply_filters( 'upserv_nonce_api_code', $code, $wp->query_vars ); + /** + * Filter the HTTP response code to be sent by the Nonce API. + * + * @param string $code The HTTP response code to be sent by the Nonce API + * @param array $request_params The request's parameters + */ + $code = apply_filters( 'upserv_nonce_api_code', $code, $wp->query_vars ); + + /** + * Filter the response to be sent by the Nonce API. + * + * @param array $response The response to be sent by the Nonce API + * @param string $code The HTTP response code sent by the Nonce API + * @param array $request_params The request's parameters + */ $response = apply_filters( 'upserv_nonce_api_response', $response, $code, $wp->query_vars ); wp_send_json( $response, $code ); } + /** + * Add query vars + * + * Add custom query variables for nonce and token endpoints. + * + * @param array $query_vars Existing query variables. + * @return array Modified query variables. + * @since 1.0.0 + */ public static function query_vars( $query_vars ) { $query_vars = array_merge( $query_vars, @@ -150,10 +280,16 @@ class Nonce { return $query_vars; } - // Overrides --------------------------------------------------- - // Misc. ------------------------------------------------------- + /** + * Create or upgrade database + * + * Create or upgrade the necessary database tables. + * + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public static function maybe_create_or_upgrade_db() { global $wpdb; @@ -191,6 +327,13 @@ class Nonce { return true; } + /** + * Register hooks + * + * Register WordPress hooks for the nonce functionality. + * + * @since 1.0.0 + */ public static function register() { if ( ! self::is_doing_api_request() ) { @@ -204,10 +347,26 @@ class Nonce { add_filter( 'query_vars', array( __CLASS__, 'query_vars' ), -99, 1 ); } + /** + * Initialize authentication + * + * Initialize the private keys used for authentication. + * + * @param array $private_keys Array of private keys. + * @since 1.0.0 + */ public static function init_auth( $private_keys ) { self::$private_keys = $private_keys; } + /** + * Check if doing API request + * + * Check if the current request is an API request. + * + * @return bool True if doing API request, false otherwise. + * @since 1.0.0 + */ public static function is_doing_api_request() { if ( null === self::$doing_api_request ) { @@ -217,6 +376,19 @@ class Nonce { return self::$doing_api_request; } + /** + * Create nonce + * + * Create a new nonce. + * + * @param bool $true_nonce Indicates if the nonce is a true nonce. + * @param int $expiry_length Time in seconds before the nonce expires. + * @param array $data Additional data to store with the nonce. + * @param int $return_type Return type (nonce only or nonce info array). + * @param bool $store Indicates if the nonce should be stored in the database. + * @return mixed The nonce or nonce info array. + * @since 1.0.0 + */ public static function create_nonce( $true_nonce = true, $expiry_length = self::DEFAULT_EXPIRY_LENGTH, @@ -224,6 +396,17 @@ class Nonce { $return_type = self::NONCE_ONLY, $store = true ) { + /** + * Filter the value of the nonce before it is created; if $nonce_value is truthy, + * the value is used as nonce and the default generation algorithm is bypassed; + * developers must respect the $return_type. + * + * @param bool|string|array $nonce_value The value of the nonce before it is created - if truthy, the nonce is considered created with this value + * @param bool $true_nonce Whether the nonce is a true, one-time-use nonce + * @param int $expiry_length The expiry length of the nonce in seconds + * @param array $data Data to store along the nonce + * @param int $return_type UPServ_Nonce::NONCE_ONLY or UPServ_Nonce::NONCE_INFO_ARRAY + */ $nonce = apply_filters( 'upserv_created_nonce', false, @@ -279,6 +462,15 @@ class Nonce { return $return; } + /** + * Get nonce expiry + * + * Get the expiry time of a nonce. + * + * @param string $nonce The nonce string. + * @return int The expiry time in seconds. + * @since 1.0.0 + */ public static function get_nonce_expiry( $nonce ) { global $wpdb; @@ -304,6 +496,15 @@ class Nonce { return intval( $nonce_expiry ); } + /** + * Get nonce data + * + * Get the data associated with a nonce. + * + * @param string $nonce The nonce string. + * @return array The nonce data. + * @since 1.0.0 + */ public static function get_nonce_data( $nonce ) { global $wpdb; @@ -329,6 +530,15 @@ class Nonce { return $data; } + /** + * Validate nonce + * + * Validate a nonce. + * + * @param string $value The nonce string. + * @return bool True if the nonce is valid, false otherwise. + * @since 1.0.0 + */ public static function validate_nonce( $value ) { if ( empty( $value ) ) { @@ -341,7 +551,15 @@ class Nonce { return $valid; } - + /** + * Delete nonce + * + * Delete a nonce from the database. + * + * @param string $value The nonce string. + * @return bool True on success, false on failure. + * @since 1.0.0 + */ public static function delete_nonce( $value ) { global $wpdb; @@ -352,6 +570,13 @@ class Nonce { return (bool) $result; } + /** + * Nonce cleanup + * + * Clean up expired nonces from the database. + * + * @since 1.0.0 + */ public static function upserv_nonce_cleanup() { if ( defined( 'WP_SETUP_CONFIG' ) || defined( 'WP_INSTALLING' ) ) { @@ -373,7 +598,21 @@ class Nonce { ) OR JSON_VALID(`data`) = 0;"; $sql_args = array( time() - self::DEFAULT_EXPIRY_LENGTH ); - $sql = apply_filters( 'upserv_clear_nonces_query', $sql, $sql_args ); + + /** + * Filter the SQL query used to clear expired nonces. + * + * @param string $sql The SQL query used to clear expired nonces + * @param array $sql_args The arguments passed to the SQL query used to clear expired nonces + */ + $sql = apply_filters( 'upserv_clear_nonces_query', $sql, $sql_args ); + + /** + * Filter the arguments passed to the SQL query used to clear expired nonces. + * + * @param array $sql_args The arguments passed to the SQL query used to clear expired nonces + * @param string $sql The SQL query used to clear expired nonces + */ $sql_args = apply_filters( 'upserv_clear_nonces_query_args', $sql_args, $sql ); $result = $wpdb->query( $wpdb->prepare( $sql, $sql_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared @@ -386,14 +625,42 @@ class Nonce { // API action -------------------------------------------------- + /** + * Generate token API response + * + * Generate a response for the token API endpoint. + * + * @param array $payload The request payload. + * @return array The API response. + * @since 1.0.0 + */ protected static function generate_token_api_response( $payload ) { return self::generate_api_response( $payload, false ); } + /** + * Generate nonce API response + * + * Generate a response for the nonce API endpoint. + * + * @param array $payload The request payload. + * @return array The API response. + * @since 1.0.0 + */ protected static function generate_nonce_api_response( $payload ) { return self::generate_api_response( $payload, true ); } + /** + * Generate API response + * + * Generate a response for the API endpoint. + * + * @param array $payload The request payload. + * @param bool $is_nonce Indicates if the response is for a nonce. + * @return array The API response. + * @since 1.0.0 + */ protected static function generate_api_response( $payload, $is_nonce ) { return self::create_nonce( $is_nonce, @@ -407,6 +674,15 @@ class Nonce { // Misc. ------------------------------------------------------- + /** + * Fetch nonce + * + * Fetch a nonce from the database. + * + * @param string $value The nonce string. + * @return string|null The nonce or null if not found. + * @since 1.0.0 + */ protected static function fetch_nonce( $value ) { global $wpdb; @@ -442,6 +718,16 @@ class Nonce { } if ( $row->expiry < time() && ! $permanent ) { + /** + * Filter whether to consider the nonce has expired. + * + * @param bool $expire_nonce Whether to consider the nonce has expired + * @param string $nonce_value The value of the nonce + * @param bool $true_nonce Whether the nonce is a true, one-time-use nonce + * @param int $expiry The timestamp at which the nonce expires + * @param array $data Data stored along the nonce + * @param object $row The database record corresponding to the nonce + */ $row->nonce = apply_filters( 'upserv_expire_nonce', null, @@ -453,6 +739,16 @@ class Nonce { ); } + /** + * Filter whether to delete the nonce. + * + * @param bool $delete Whether to delete the nonce + * @param string $nonce_value The value of the nonce + * @param bool $true_nonce Whether the nonce is a true, one-time-use nonce + * @param int $expiry The timestamp at which the nonce expires + * @param array $data Data stored along the nonce + * @param object $row The database record corresponding to the nonce + */ $delete_nonce = apply_filters( 'upserv_delete_nonce', $row->true_nonce || null === $row->nonce, @@ -466,11 +762,32 @@ class Nonce { self::delete_nonce( $value ); } + /** + * Filter the value of the nonce after it has been fetched from the database. + * + * @param string $nonce_value The value of the nonce after it has been fetched from the database + * @param bool $true_nonce Whether the nonce is a true, one-time-use nonce + * @param int $expiry The timestamp at which the nonce expires + * @param array $data Data stored along the nonce + * @param object $row The database record corresponding to the nonce + */ $nonce = apply_filters( 'upserv_fetch_nonce', $row->nonce, $row->true_nonce, $row->expiry, $data, $row ); return $nonce; } + /** + * Store nonce + * + * Store a nonce in the database. + * + * @param string $nonce The nonce string. + * @param bool $true_nonce Indicates if the nonce is a true nonce. + * @param int $expiry The expiry time in seconds. + * @param string $data The nonce data. + * @return array|false The stored nonce data or false on failure. + * @since 1.0.0 + */ protected static function store_nonce( $nonce, $true_nonce, $expiry, $data ) { global $wpdb; @@ -489,6 +806,14 @@ class Nonce { return false; } + /** + * Generate ID + * + * Generate a unique ID. + * + * @return string The generated ID. + * @since 1.0.0 + */ protected static function generate_id() { require_once ABSPATH . 'wp-includes/class-phpass.php'; @@ -497,6 +822,14 @@ class Nonce { return md5( $hasher->get_random_bytes( 100, false ) ); } + /** + * Authorize request + * + * Authorize the incoming request using the provided credentials and signature. + * + * @return bool True if the request is authorized, false otherwise. + * @since 1.0.0 + */ protected static function authorize() { $sign = false; $key_id = false; @@ -560,6 +893,13 @@ class Nonce { $auth = hash_equals( $values['signature'], $sign ); } + /** + * Filter whether the request for a nonce is authorized. + * + * @param bool $authorized Whether the request is authorized + * @param string $received_key The key use to attempt the authorization + * @param array $private_auth_keys The valid authorization keys + */ return apply_filters( 'upserv_nonce_authorize', $auth, diff --git a/inc/scheduler/class-scheduler.php b/inc/scheduler/class-scheduler.php index 8086cd6..4668bf3 100644 --- a/inc/scheduler/class-scheduler.php +++ b/inc/scheduler/class-scheduler.php @@ -8,9 +8,26 @@ if ( ! defined( 'ABSPATH' ) ) { use WP_Error; +/** + * Scheduler class + * + * @since 1.0.0 + */ class Scheduler { + /** + * Instance + * + * @var Scheduler|null + * @since 1.0.0 + */ protected static $instance = null; + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks. + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { if ( $init_hooks ) { @@ -19,6 +36,14 @@ class Scheduler { } } + /** + * Get instance + * + * Retrieve or create the Scheduler singleton instance. + * + * @return Scheduler The scheduler instance. + * @since 1.0.0 + */ public static function get_instance() { if ( ! self::$instance ) { @@ -28,6 +53,16 @@ class Scheduler { return self::$instance; } + /** + * Magic method handler + * + * Routes method calls to either ActionScheduler functions or native WordPress functions. + * + * @param string $name The method name. + * @param array $arguments The method arguments. + * @return mixed|WP_Error The result of the method call or error if method doesn't exist. + * @since 1.0.0 + */ public function __call( $name, $arguments ) { if ( ! method_exists( $this, $name ) ) { @@ -51,10 +86,24 @@ class Scheduler { return $this->$name( ...$arguments ); } + /** + * Action scheduler initialization + * + * Fires when the Action Scheduler is initialized. + * + * @since 1.0.0 + */ public function action_scheduler_init() { do_action( 'upserv_scheduler_init' ); } + /** + * Initialize + * + * Handles plugin initialization logic. + * + * @since 1.0.0 + */ public function init() { if ( ! class_exists( 'ActionScheduler', false ) ) { @@ -62,6 +111,20 @@ class Scheduler { } } + /** + * Schedule single action + * + * Schedule a one-time action event. + * + * @param int $timestamp When the action should run (Unix timestamp). + * @param string $hook The hook to execute. + * @param array $args Arguments to pass to the hook's callback. + * @param string $group The group to assign this action to. + * @param bool $unique Whether to ensure this action is unique. + * @param int $priority The priority of the action. + * @return bool|int The action ID or false if not scheduled. + * @since 1.0.0 + */ protected function schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( $unique ) { @@ -71,6 +134,21 @@ class Scheduler { return wp_schedule_single_event( $timestamp, $hook, $args ); } + /** + * Schedule recurring action + * + * Schedule a repeating action event. + * + * @param int $timestamp When the action should first run (Unix timestamp). + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook The hook to execute. + * @param array $args Arguments to pass to the hook's callback. + * @param string $group The group to assign this action to. + * @param bool $unique Whether to ensure this action is unique. + * @param int $priority The priority of the action. + * @return bool|int The action ID or false if not scheduled. + * @since 1.0.0 + */ protected function schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( $unique ) { @@ -101,6 +179,17 @@ class Scheduler { return wp_schedule_event( $timestamp, $interval, $hook, $args ); } + /** + * Unschedule all actions + * + * Cancel all scheduled instances of a specific action. + * + * @param string $hook The action hook to unschedule. + * @param array $args Args matching those of the action to unschedule. + * @param string $group The group to which the action belongs. + * @return void + * @since 1.0.0 + */ protected function unschedule_all_actions( $hook, $args = array(), $group = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $timestamp = wp_next_scheduled( $hook, $args ); @@ -110,10 +199,32 @@ class Scheduler { } } + /** + * Get next scheduled action + * + * Retrieve the next timestamp for a scheduled action. + * + * @param string $hook The hook to check. + * @param array $args Args matching those of the action to check. + * @param string $group The group to which the action belongs. + * @return int|false The timestamp for the next occurrence or false if not scheduled. + * @since 1.0.0 + */ protected function next_scheduled_action( $hook, $args = array(), $group = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return wp_next_scheduled( $hook, $args ); } + /** + * Check if action is scheduled + * + * Determine whether an action is currently scheduled. + * + * @param string $hook The hook to check. + * @param array $args Args matching those of the action to check. + * @param string $group The group to which the action belongs. + * @return bool Whether the action is scheduled. + * @since 1.0.0 + */ protected function has_scheduled_action( $hook, $args = array(), $group = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return (bool) wp_next_scheduled( $hook, $args ); } diff --git a/inc/server/license/class-license-server.php b/inc/server/license/class-license-server.php index 11ec95c..3812f21 100644 --- a/inc/server/license/class-license-server.php +++ b/inc/server/license/class-license-server.php @@ -13,8 +13,19 @@ use WP_Error; use Anyape\Crypto\Crypto; use Anyape\Utils\Utils; +/** + * License Server class + * + * @since 1.0.0 + */ class License_Server { + /** + * License definition template + * + * @var array + * @since 1.0.0 + */ public static $license_definition = array( 'id' => 0, 'license_key' => '', @@ -32,13 +43,27 @@ class License_Server { 'package_type' => '', 'data' => array(), ); - public static $browsing_query = array( + + /** + * Default browsing query settings + * + * @var array + * @since 1.0.0 + */ + public static $browsing_query = array( 'relationship' => 'AND', 'limit' => 999, 'offset' => 0, 'order_by' => 'date_created', 'criteria' => array(), ); + + /** + * Supported browsing operators + * + * @var array + * @since 1.0.0 + */ public static $browsing_operators = array( '=', '!=', @@ -53,7 +78,14 @@ class License_Server { 'LIKE', 'NOT LIKE', ); - public static $license_statuses = array( + + /** + * Supported license statuses + * + * @var array + * @since 1.0.0 + */ + public static $license_statuses = array( 'pending', 'activated', 'deactivated', @@ -62,12 +94,26 @@ class License_Server { 'expired', ); + /** + * Constructor + * + * @since 1.0.0 + */ public function __construct() {} /******************************************************************* * Public methods *******************************************************************/ + /** + * Build license payload + * + * Creates a properly structured license payload from input data. + * + * @param array $payload The raw license data. + * @return array The processed license payload. + * @since 1.0.0 + */ public function build_license_payload( $payload ) { $payload = $this->extend_license_payload( $this->filter_license_payload( $payload ) ); @@ -76,11 +122,27 @@ class License_Server { return $this->cleanup_license_payload( $payload ); } + /** + * Browse licenses + * + * Retrieve multiple licenses based on query criteria. + * + * @param array $payload The query parameters. + * @return array|WP_Error Array of licenses or WP_Error on failure. + * @since 1.0.0 + */ public function browse_licenses( $payload ) { global $wpdb; $prepare_args = array(); - $payload = apply_filters( 'upserv_browse_licenses_payload', $payload ); + /** + * Filter the payload used to browse licenses - before the payload has been cleaned up and the License Query has been validated. + * Fired during client license API request. + * + * @param array $payload A dirty payload for a License Query + * @since 1.0.0 + */ + $payload = apply_filters( 'upserv_browse_licenses_payload', $payload ); try { $browsing_query = $this->build_browsing_query( $payload ); @@ -138,11 +200,29 @@ class License_Server { } } + /** + * Fired after browsing license records. + * Fired during client license API request. + * + * @param array $licenses The license records retrieved or an empty array + * @param array $payload The payload of the request + * @since 1.0.0 + */ do_action( 'upserv_did_browse_licenses', $licenses, $payload ); return $licenses; } + /** + * Read license + * + * Retrieve a single license by ID or license key. + * + * @param array $payload The query parameters containing ID or license key. + * @param bool $force Whether to bypass cache. + * @return object|WP_Error License object or WP_Error on failure. + * @since 1.0.0 + */ public function read_license( $payload, $force = false ) { $where_field = isset( $payload['license_key'] ) ? 'license_key' : 'id'; $where_value = isset( $payload[ $where_field ] ) ? $payload[ $where_field ] : null; @@ -151,7 +231,14 @@ class License_Server { $validation = $this->validate_license_payload( $payload, true ); if ( ( $force || ! $found ) && true === $validation ) { - $payload = $this->filter_license_payload( $payload ); + $payload = $this->filter_license_payload( $payload ); + /** + * Filter the payload used to read a license record - after the payload has been cleaned up, before the payload has been validated. + * Fired during client license API request. + * + * @param array $payload Payload used to read a license record + * @since 1.0.0 + */ $payload = apply_filters( 'upserv_read_license_payload', $payload ); $validation = $this->validate_license_payload( $payload, true ); $return = $validation; @@ -177,13 +264,37 @@ class License_Server { $return = $validation; } + /** + * Fired after reading a license record. + * Fired during client license API request. + * + * @param mixed $return The result of the operation - a license object record or an empty array + * @param array $payload The payload of the request + * @since 1.0.0 + */ do_action( 'upserv_did_read_license', $return, $payload ); return $return; } + /** + * Edit license + * + * Update an existing license. + * + * @param array $payload The license data to update. + * @return object|WP_Error Updated license object or WP_Error on failure. + * @since 1.0.0 + */ public function edit_license( $payload ) { - $payload = $this->cleanup_license_payload( $this->filter_license_payload( $payload ) ); + $payload = $this->cleanup_license_payload( $this->filter_license_payload( $payload ) ); + /** + * Filter the payload used to edit a license record - after the payload has been cleaned up, before the payload has been validated. + * Fired during client license API request. + * + * @param array $payload Payload used to edit a license record + * @since 1.0.0 + */ $payload = apply_filters( 'upserv_edit_license_payload', $payload ); $validation = $this->validate_license_payload( $payload, true ); $return = $validation; @@ -221,13 +332,38 @@ class License_Server { } } + /** + * Fired after editing a license record. + * Fired during client license API request. + * + * @param mixed $return The result of the operation - a license record object or an array of errors + * @param array $payload The payload of the request + * @param mixed $original The original record to edit - a license record object or an array of errors + * @since 1.0.0 + */ do_action( 'upserv_did_edit_license', $return, $payload, $original ); return $return; } + /** + * Add license + * + * Create a new license. + * + * @param array $payload The license data. + * @return object|WP_Error New license object or WP_Error on failure. + * @since 1.0.0 + */ public function add_license( $payload ) { - $payload = $this->build_license_payload( $payload ); + $payload = $this->build_license_payload( $payload ); + /** + * Filter the payload used to add a license record - after the payload has been cleaned up, before the payload has been validated. + * Fired during client license API request. + * + * @param array $payload Payload used to add a license record + * @since 1.0.0 + */ $payload = apply_filters( 'upserv_add_license_payload', $payload ); $validation = $this->validate_license_payload( $payload ); $return = $validation; @@ -266,13 +402,37 @@ class License_Server { } } + /** + * Fired after adding a license record. + * Fired during client license API request. + * + * @param mixed $return The result of the operation - a license record object or an array of errors + * @param array $payload The payload of the request + * @since 1.0.0 + */ do_action( 'upserv_did_add_license', $return, $payload ); return $return; } + /** + * Delete license + * + * Remove a license from the system. + * + * @param array $payload The license identifier data. + * @return object|WP_Error Deleted license object or WP_Error on failure. + * @since 1.0.0 + */ public function delete_license( $payload ) { - $payload = $this->filter_license_payload( $payload ); + $payload = $this->filter_license_payload( $payload ); + /** + * Filter the payload used to delete a license record - after the payload has been cleaned up, before the payload has been validated. + * Fired during client license API request. + * + * @param array $payload Payload used to delete a license record + * @since 1.0.0 + */ $payload = apply_filters( 'upserv_delete_license_payload', $payload ); $validation = $this->validate_license_payload( $payload, true ); $return = $validation; @@ -305,11 +465,29 @@ class License_Server { } } + /** + * Fired after deleting a license record. + * Fired during client license API request. + * + * @param mixed $return The result of the operation - a license record object or an empty array + * @param array $payload The payload of the request + * @since 1.0.0 + */ do_action( 'upserv_did_delete_license', $return, $payload ); return $return; } + /** + * Generate license signature + * + * Create a cryptographic signature for a license and domain. + * + * @param object $license The license object. + * @param string $domain The domain to generate signature for. + * @return string The generated signature. + * @since 1.0.0 + */ public function generate_license_signature( $license, $domain ) { $hmac_key = $license->hmac_key; $crypto_key = $license->crypto_key; @@ -319,6 +497,16 @@ class License_Server { return $signature; } + /** + * Is signature valid + * + * Verify if a license signature is valid. + * + * @param string $license_key The license key. + * @param string $license_signature The signature to validate. + * @return bool Whether the signature is valid. + * @since 1.0.0 + */ public function is_signature_valid( $license_key, $license_signature ) { $valid = false; $crypt = $license_signature; @@ -344,7 +532,15 @@ class License_Server { in_array( $domain, $license->allowed_domains, true ) && $license->package_slug === $package_slug ) { - $valid = true; + /** + * Filter whether to bypass the license signature check. + * Fired during client license API request. + * + * @param bool $bypass Whether to bypass the license signature check + * @param object $license The license object + * @since 1.0.0 + */ + $valid = apply_filters( 'upserv_license_bypass_signature', true, $license ); } } } @@ -352,6 +548,13 @@ class License_Server { return $valid; } + /** + * Switch expired licenses status + * + * Update status of licenses that have reached their expiry date. + * + * @since 1.0.0 + */ public function switch_expired_licenses_status() { global $wpdb; @@ -401,6 +604,15 @@ class License_Server { $item->data['operation'] = 'edit'; $item->data['operation_id'] = bin2hex( random_bytes( 16 ) ); + /** + * Fired after editing a license record. + * Fired during client license API request. + * + * @param mixed $item The result of the operation - a license record object or an array of errors + * @param array $payload The payload of the request + * @param mixed $original The original record to edit - a license record object or an array of errors + * @since 1.0.0 + */ do_action( 'upserv_did_edit_license', $item, @@ -414,6 +626,15 @@ class License_Server { } } + /** + * Update licenses status + * + * Bulk update status for multiple licenses. + * + * @param string $status The new status to set. + * @param array $license_ids Optional array of license IDs to update. + * @since 1.0.0 + */ public function update_licenses_status( $status, $license_ids = array() ) { $license_query = array( 'limit' => '-1' ); @@ -455,6 +676,15 @@ class License_Server { $item->data['operation'] = 'edit'; $item->data['operation_id'] = bin2hex( random_bytes( 16 ) ); + /** + * Fired after editing a license record. + * Fired during client license API request. + * + * @param mixed $item The result of the operation - a license record object or an array of errors + * @param array $payload The payload of the request + * @param mixed $original The original record to edit - a license record object or an array of errors + * @since 1.0.0 + */ do_action( 'upserv_did_edit_license', $item, @@ -468,6 +698,14 @@ class License_Server { } } + /** + * Purge licenses + * + * Delete licenses from the database. + * + * @param array $license_ids Optional array of license IDs to delete. + * @since 1.0.0 + */ public function purge_licenses( $license_ids = array() ) { $license_query = array( 'limit' => '-1' ); @@ -509,6 +747,14 @@ class License_Server { $item->data['operation'] = 'delete'; $item->data['operation_id'] = bin2hex( random_bytes( 16 ) ); + /** + * Fired after deleting a license record. + * Fired during client license API request. + * + * @param mixed $item The result of the operation - a license record object or an empty array + * @param array $payload The payload of the request + * @since 1.0.0 + */ do_action( 'upserv_did_delete_license', $item, array( $item->license_key ) ); } } @@ -518,6 +764,16 @@ class License_Server { * Protected methods *******************************************************************/ + /** + * Build browsing query + * + * Construct a valid query structure for browsing licenses. + * + * @param array $payload The raw query parameters. + * @return array The processed query structure. + * @throws Exception If query parameters are invalid. + * @since 1.0.0 + */ protected function build_browsing_query( $payload ) { $original = $payload; $payload = array_intersect_key( $payload, self::$browsing_query ); @@ -669,6 +925,15 @@ class License_Server { return $payload; } + /** + * Cleanup license payload + * + * Fill in default values for missing license data. + * + * @param array $payload The license data to clean up. + * @return array The processed license data. + * @since 1.0.0 + */ protected function cleanup_license_payload( $payload ) { if ( isset( $payload['license_key'] ) && empty( $payload['license_key'] ) ) { @@ -688,14 +953,41 @@ class License_Server { return $payload; } + /** + * Filter license payload + * + * Remove any properties not in the license definition. + * + * @param array $payload The license data to filter. + * @return array The filtered license data. + * @since 1.0.0 + */ protected function filter_license_payload( $payload ) { return is_array( $payload ) ? array_intersect_key( $payload, self::$license_definition ) : self::$license_definition; } + /** + * Extend license payload + * + * Add default values for missing properties in license data. + * + * @param array $payload The license data to extend. + * @return array The extended license data. + * @since 1.0.0 + */ protected function extend_license_payload( $payload ) { return array_merge( self::$license_definition, $payload ); } + /** + * Sanitize payload + * + * Clean and validate license data. + * + * @param array $license The license data to sanitize. + * @return array The sanitized license data. + * @since 1.0.0 + */ protected function sanitize_payload( $license ) { foreach ( $license as $key => $value ) { @@ -749,6 +1041,16 @@ class License_Server { return $license; } + /** + * Validate license payload + * + * Check if license data is valid. + * + * @param array $license The license data to validate. + * @param bool $partial Whether to perform partial validation. + * @return bool|array True if valid, array of errors otherwise. + * @since 1.0.0 + */ protected function validate_license_payload( $license, $partial = false ) { global $wpdb; @@ -835,7 +1137,20 @@ class License_Server { ! ( $partial && ! isset( $license['status'] ) ) && ! in_array( $license['status'], self::$license_statuses, true ) ) { - $errors['invalid_status'] = __( 'The license status is invalid.', 'updatepulse-server' ); + /** + * Filter whether a license is valid when requesting for an update. + * Fired during client license API request. + * + * @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 + * @since 1.0.0 + */ + $valid_status = apply_filters( 'upserv_license_valid', false, $license, '' ); + + if ( ! $valid_status ) { + $errors['invalid_status'] = __( 'The license status is invalid.', 'updatepulse-server' ); + } } if ( diff --git a/inc/server/update/class-cache.php b/inc/server/update/class-cache.php index 0826d8f..bc88c52 100644 --- a/inc/server/update/class-cache.php +++ b/inc/server/update/class-cache.php @@ -9,13 +9,33 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Cache class. * - * Cache data to the filesystem. + * @since 1.0.0 */ class Cache { + /** + * Cache directory path + * + * @var string + * @since 1.0.0 + */ protected $cache_directory; + + /** + * File extension for cache files + * + * @var string + * @since 1.0.0 + */ protected $extension; + /** + * Constructor + * + * @param string $cache_directory Directory to store cache files. + * @param string $extension File extension for cache files. Default 'dat'. + * @since 1.0.0 + */ public function __construct( $cache_directory, $extension = 'dat' ) { $this->cache_directory = $cache_directory; $this->extension = $extension; @@ -24,8 +44,11 @@ class Cache { /** * Get cached value. * - * @param string $key - * @return mixed|null + * Retrieves a value from the cache if it exists and hasn't expired. + * + * @param string $key Cache key identifier. + * @return mixed|null Cached value or null if not found or expired. + * @since 1.0.0 */ public function get( $key ) { $filename = $this->get_cache_filename( $key ); @@ -53,10 +76,13 @@ class Cache { /** * Update the cache. * - * @param string $key Cache key. + * Stores a value in the cache with the specified expiration time. + * + * @param string $key Cache key identifier. * @param mixed $value The value to store in the cache. * @param int $expiration Time until expiration, in seconds. Optional. Default `0`. * @return void + * @since 1.0.0 */ public function set( $key, $value, $expiration = 0 ) { $cache = array( @@ -75,8 +101,11 @@ class Cache { /** * Clear the cache by key. * - * @param string $key Cache key. + * Removes a specific cached value by its key. + * + * @param string $key Cache key identifier. * @return void + * @since 1.0.0 */ public function clear( $key ) { $file = $this->get_cache_filename( $key ); @@ -87,8 +116,13 @@ class Cache { } /** - * @param string $key - * @return string + * Get cache filename + * + * Constructs the full path to a cache file based on its key. + * + * @param string $key Cache key identifier. + * @return string Full path to the cache file. + * @since 1.0.0 */ protected function get_cache_filename( $key ) { return $this->cache_directory . '/' . $key . '.' . $this->extension; diff --git a/inc/server/update/class-headers.php b/inc/server/update/class-headers.php index b26299c..4b17a7d 100644 --- a/inc/server/update/class-headers.php +++ b/inc/server/update/class-headers.php @@ -12,6 +12,11 @@ use Countable; use ArrayIterator; use Traversable; +/** + * Headers class + * + * @since 1.0.0 + */ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** @@ -19,6 +24,7 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { * These special headers don't have that prefix, so we need an explicit list to identify them. * * @var array + * @since 1.0.0 */ protected static $unprefixed_names = array( 'CONTENT_TYPE', @@ -29,8 +35,24 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { 'AUTH_TYPE', ); + /** + * Headers collection + * + * Stores all HTTP headers. + * + * @var array + * @since 1.0.0 + */ protected $headers = array(); + /** + * Constructor + * + * Initialize headers from provided array. + * + * @param array $headers Initial headers to set. + * @since 1.0.0 + */ public function __construct( $headers = array() ) { foreach ( $headers as $name => $value ) { @@ -41,8 +63,9 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** * Extract HTTP headers from an array of data ( usually $_SERVER ). * - * @param array $environment - * @return array + * @param array $environment Server environment variables. + * @return array Extracted HTTP headers. + * @since 1.0.0 */ protected static function parse_server() { $results = array(); @@ -65,8 +88,9 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** * Check if a $_SERVER key looks like a HTTP header name. * - * @param string $key - * @return bool + * @param string $key The key to check. + * @return bool Whether the key is a HTTP header name. + * @since 1.0.0 */ protected static function is_header_name( $key ) { return ( @@ -80,7 +104,8 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { * Parse headers for the current HTTP request. * Will automatically choose the best way to get the headers from PHP. * - * @return array + * @return array HTTP headers from the current request. + * @since 1.0.0 */ public static function parse_current() { @@ -98,8 +123,9 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** * Convert a header name to "Title-Case-With-Dashes". * - * @param string $name - * @return string + * @param string $name Header name to normalize. + * @return string Normalized header name. + * @since 1.0.0 */ protected function normalize_name( $name ) { $name = strtolower( $name ); @@ -113,9 +139,10 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** * Check if a string starts with the given prefix. * - * @param string $string - * @param string $prefix - * @return bool + * @param string $_string The string to check. + * @param string $prefix The prefix to look for. + * @return bool Whether the string starts with the prefix. + * @since 1.0.0 */ protected static function starts_with( $_string, $prefix ) { return ( substr( $_string, 0, strlen( $prefix ) ) === $prefix ); @@ -126,7 +153,8 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { * * @param string $name Header name. * @param mixed $_default The default value to return if the header doesn't exist. - * @return string|null + * @return string|null Header value or default if not found. + * @since 1.0.0 */ public function get( $name, $_default = null ) { $name = $this->normalize_name( $name ); @@ -141,8 +169,9 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /** * Set a header to value. * - * @param string $name - * @param string $value + * @param string $name Header name. + * @param string $value Header value. + * @since 1.0.0 */ public function set( $name, $value ) { $name = $this->normalize_name( $name ); @@ -151,34 +180,83 @@ class Headers implements ArrayAccess, IteratorAggregate, Countable { /* ArrayAccess interface */ + /** + * Check if header exists + * + * Implementation for ArrayAccess interface. + * + * @param mixed $offset The header name. + * @return bool Whether the header exists. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function offsetExists( $offset ): bool { return array_key_exists( $offset, $this->headers ); } + /** + * Get header value + * + * Implementation for ArrayAccess interface. + * + * @param mixed $offset The header name. + * @return mixed The header value. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function offsetGet( $offset ): mixed { return $this->get( $offset ); } + /** + * Set header value + * + * Implementation for ArrayAccess interface. + * + * @param mixed $offset The header name. + * @param mixed $value The header value. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function offsetSet( $offset, $value ): void { $this->set( $offset, $value ); } + /** + * Unset header + * + * Implementation for ArrayAccess interface. + * + * @param mixed $offset The header name. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function offsetUnset( $offset ): void { $name = $this->normalize_name( $offset ); unset( $this->headers[ $name ] ); } - /* Countable interface */ + /** + * Count headers + * + * Implementation for Countable interface. + * + * @return int Number of headers. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function count(): int { return count( $this->headers ); } - /* IteratorAggregate interface */ + /** + * Get iterator for headers + * + * Implementation for IteratorAggregate interface. + * + * @return Traversable Iterator for headers. + * @since 1.0.0 + */ #[\ReturnTypeWillChange] public function getIterator(): Traversable { return new ArrayIterator( $this->headers ); diff --git a/inc/server/update/class-package.php b/inc/server/update/class-package.php index 1048b67..0d7649b 100644 --- a/inc/server/update/class-package.php +++ b/inc/server/update/class-package.php @@ -10,16 +10,32 @@ if ( ! defined( 'ABSPATH' ) ) { * This class represents the collection of files and metadata that make up * a WordPress plugin or theme, or a generic software package. * + * @since 1.0.0 */ class Package { - /** @var string Path to the Zip archive that contains the package. */ + /** + * Path to the Zip archive that contains the package. + * + * @var string + * @since 1.0.0 + */ protected $filename; - /** @var array Package metadata in a format suitable for the update checker. */ + /** + * Package metadata in a format suitable for the update checker. + * + * @var array + * @since 1.0.0 + */ protected $metadata = array(); - /** @var string Package slug. */ + /** + * Package slug. + * + * @var string + * @since 1.0.0 + */ public $slug; /** @@ -29,9 +45,10 @@ class Package { * of instantiating this class directly. Still, you can do it if you want to, for example, * load package metadata from the database instead of extracting it from a Zip file. * - * @param string $slug - * @param string $filename - * @param array $metadata + * @param string $slug The package slug. + * @param string $filename The path to the package file. + * @param array $metadata The package metadata. + * @since 1.0.0 */ public function __construct( $slug, $filename = null, $metadata = array() ) { $this->slug = $slug; @@ -42,7 +59,8 @@ class Package { /** * Get the full file path of this package. * - * @return string + * @return string The full file path of the package. + * @since 1.0.0 */ public function get_filename() { return $this->filename; @@ -52,7 +70,8 @@ class Package { * Get package metadata. * * @see self::extractMetadata() - * @return array + * @return array The package metadata merged with the slug. + * @since 1.0.0 */ public function get_metadata() { return array_merge( $this->metadata, array( 'slug' => $this->slug ) ); @@ -63,10 +82,11 @@ class Package { * * @param string $filename Path to a Zip archive that contains a package. * @param string $slug Optional package slug. Will be detected automatically. - * @param Cache $cache - * @return Package + * @param Cache|null $cache Optional cache object for metadata. + * @return Package A new Package instance with the extracted metadata. + * @since 1.0.0 */ - public static function from_archive( $filename, $slug = null, Cache $cache = null ) { + public static function from_archive( $filename, $slug = null, $cache = null ) { $meta_obj = new Zip_Metadata_Parser( $slug, $filename, $cache ); $metadata = $meta_obj->get(); @@ -80,7 +100,8 @@ class Package { /** * Get the size of the package (in bytes). * - * @return int + * @return int The size of the package file in bytes. + * @since 1.0.0 */ public function get_file_size() { return filesize( $this->filename ); @@ -89,7 +110,8 @@ class Package { /** * Get the Unix timestamp of the last time this package was modified. * - * @return int + * @return int The Unix timestamp when the package was last modified. + * @since 1.0.0 */ public function get_last_modified() { return filemtime( $this->filename ); diff --git a/inc/server/update/class-request.php b/inc/server/update/class-request.php index a062470..aa7c3a5 100644 --- a/inc/server/update/class-request.php +++ b/inc/server/update/class-request.php @@ -8,29 +8,94 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Simple request class for the update server. + * + * Handles incoming update requests, parsing parameters and headers. */ class Request { - /** @var array Query parameters. */ + /** + * Query parameters + * + * @var array + * @since 1.0.0 + */ public $query = array(); - /** @var string Client's IP address. */ + /** + * Client's IP address + * + * @var string + * @since 1.0.0 + */ public $client_ip; - /** @var string The HTTP method, e.g. "POST" or "GET". */ + /** + * The HTTP method + * + * @var string + * @since 1.0.0 + */ public $http_method; - /** @var string The name of the current action. For example, "get_metadata". */ + /** + * The name of the current action + * + * @var string + * @since 1.0.0 + */ public $action; - /** @var string Package slug from the current request. */ + /** + * Package slug from the current request + * + * @var string + * @since 1.0.0 + */ public $slug; - /** @var Package The package that matches the current slug, if any. */ + /** + * The package that matches the current slug + * + * @var Package|null + * @since 1.0.0 + */ public $package = null; - /** @var string WordPress version number as extracted from the User-Agent header. */ + /** + * WordPress version number + * + * @var string|null + * @since 1.0.0 + */ public $wp_version = null; - /** @var string WordPress site URL, also from the User-Agent. */ + /** + * WordPress site URL + * + * @var string|null + * @since 1.0.0 + */ public $wp_site_url = null; + /** + * Request headers container + * + * @var Headers + * @since 1.0.0 + */ + public $headers; - /** @var array Other, arbitrary request properties. */ + /** + * Other, arbitrary request properties + * + * @var array + * @since 1.0.0 + */ protected $props = array(); + /** + * Constructor + * + * Initialize a new request object with query parameters, headers and connection info. + * + * @param array $query Request query parameters. + * @param array $headers Request HTTP headers. + * @param string $client_ip Client's IP address, defaults to '0.0.0.0'. + * @param string $http_method HTTP method used for the request, defaults to 'GET'. + * @since 1.0.0 + */ public function __construct( $query, $headers, $client_ip = '0.0.0.0', $http_method = 'GET' ) { $this->query = $query; $this->headers = new Headers( $headers ); @@ -58,9 +123,12 @@ class Request { /** * Get the value of a query parameter. * - * @param string $name Parameter name. + * Safely retrieves a parameter from the query array with an optional default value. + * + * @param string $name Parameter name to retrieve. * @param mixed $_default The value to return if the parameter doesn't exist. Defaults to null. - * @return mixed + * @return mixed The parameter value or default if not found. + * @since 1.0.0 */ public function param( $name, $_default = null ) { @@ -71,6 +139,15 @@ class Request { } } + /** + * Magic getter for dynamic properties + * + * Retrieves dynamically stored properties from the props array. + * + * @param string $name Property name to retrieve. + * @return mixed The property value or null if not found. + * @since 1.0.0 + */ public function __get( $name ) { if ( array_key_exists( $name, $this->props ) ) { @@ -80,14 +157,40 @@ class Request { return null; } + /** + * Magic setter for dynamic properties + * + * Sets values in the dynamic props array. + * + * @param string $name Property name to set. + * @param mixed $value Value to assign to the property. + * @since 1.0.0 + */ public function __set( $name, $value ) { $this->props[ $name ] = $value; } + /** + * Magic isset checker for dynamic properties + * + * Checks if a dynamic property exists in the props array. + * + * @param string $name Property name to check. + * @return bool Whether the property exists. + * @since 1.0.0 + */ public function __isset( $name ) { return isset( $this->props[ $name ] ); } + /** + * Magic unset for dynamic properties + * + * Removes a property from the props array. + * + * @param string $name Property name to remove. + * @since 1.0.0 + */ public function __unset( $name ) { unset( $this->props[ $name ] ); } From 58131d74f72ba9565da02fb9743ce506c1d8d97d Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:51:47 +0800 Subject: [PATCH 35/49] Full documentation WIP - this file needs actions doc --- inc/server/update/class-update-server.php | 625 ++++++++++++++++++++-- 1 file changed, 587 insertions(+), 38 deletions(-) diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php index fdaaa69..af3cb4c 100644 --- a/inc/server/update/class-update-server.php +++ b/inc/server/update/class-update-server.php @@ -21,27 +21,146 @@ 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; @@ -62,9 +181,12 @@ class Update_Server { *******************************************************************/ /** - * Process an update API request. + * Process an update API request * - * @param array $query Query parameters. + * 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 ); @@ -80,22 +202,60 @@ class Update_Server { // 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; + $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 ); @@ -133,7 +293,25 @@ class Update_Server { 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 ); do_action( 'upserv_filter_package_info', $info ); @@ -141,6 +319,16 @@ class Update_Server { 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; @@ -157,15 +345,24 @@ class Update_Server { try { $info = $this->update_checker->request_info(); - if ( - ! apply_filters( - 'upserv_download_remote_package', - ! ( is_array( $info ) && isset( $info['abort_request'] ) && $info['abort_request'] ), - $safe_slug, - $this->type, - $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 ); do_action( 'upserv_download_remote_package_aborted', $safe_slug, $this->type, $info ); @@ -208,6 +405,12 @@ class Update_Server { 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; @@ -216,6 +419,15 @@ class Update_Server { } } + /** + * 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 ) { do_action( 'upserv_check_remote_update', $slug ); @@ -224,7 +436,15 @@ class Update_Server { if ( $local_package instanceof Package ) { $package_path = $local_package->get_filename(); - $meta = apply_filters( + /** + * 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, @@ -232,6 +452,14 @@ class Update_Server { ); 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, @@ -268,6 +496,16 @@ class Update_Server { 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(); @@ -295,6 +533,14 @@ class Update_Server { $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 ) { @@ -317,12 +563,14 @@ class Update_Server { *******************************************************************/ /** - * Add one or more query arguments to a URL. - * Setting an argument to `null` removes it. + * Add query arguments to a URL * - * @param array $args An associative array of query arguments. - * @param string $url The old URL. Optional, defaults to the request url without query arguments. - * @return string New 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 ) { @@ -349,6 +597,14 @@ class Update_Server { 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 ) { @@ -360,6 +616,15 @@ class Update_Server { } } + /** + * 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(); @@ -387,6 +652,14 @@ class Update_Server { 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 ( @@ -401,6 +674,15 @@ class Update_Server { $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(); @@ -418,9 +700,26 @@ class Update_Server { 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 ) { 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; } @@ -441,9 +740,12 @@ class Update_Server { } /** - * Basic request validation. Every request must specify an action and a valid package slug. + * Validate request parameters * - * @param Wpup_Request $request + * Check if the request contains required parameters. + * + * @param Request $request Request instance + * @since 1.0.0 */ protected function validate_request( $request ) { @@ -461,9 +763,12 @@ class Update_Server { } /** - * Load the requested package into the request instance. + * Load package for request * - * @param Wpup_Request $request + * Find and load the requested package. + * + * @param Request $request Request instance + * @since 1.0.0 */ protected function load_package_for( $request ) { @@ -486,15 +791,36 @@ class Update_Server { } } + /** + * 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'; + $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 ), @@ -536,6 +862,14 @@ class Update_Server { 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(); @@ -555,6 +889,16 @@ class Update_Server { 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, @@ -571,10 +915,13 @@ class Update_Server { } /** + * Normalize file path + * * Convert all directory separators to forward slashes. * - * @param string $path - * @return string + * @param string $path File path + * @return string Normalized path + * @since 1.0.0 */ protected function normalize_file_path( $path ) { @@ -586,9 +933,12 @@ class Update_Server { } /** - * Log an API request. + * Log a request * - * @param Wpup_Request $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(); @@ -629,7 +979,12 @@ class Update_Server { } /** - * @return string + * 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'; @@ -640,20 +995,26 @@ class Update_Server { } /** - * Escapes passed log data so it can be safely written into a plain text file. + * Escape log information * - * @param string[] $columns List of columns in the log entry. - * @return string[] Escaped $columns. + * 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 ); } /** - * Escapes passed value to be safely written into a plain text file. + * Escape log value * - * @param string|null $value Value to escape. - * @return string|null Escaped 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 ) { @@ -690,6 +1051,15 @@ class Update_Server { 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. @@ -749,9 +1119,12 @@ class Update_Server { } /** - * Output data as JSON. + * Output data as JSON * - * @param mixed $response + * 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' ); @@ -761,6 +1134,14 @@ class Update_Server { // 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(); @@ -772,6 +1153,14 @@ class Update_Server { 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(); @@ -783,6 +1172,15 @@ class Update_Server { } } + /** + * 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(); @@ -790,6 +1188,16 @@ class Update_Server { 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' ); @@ -809,7 +1217,27 @@ class Update_Server { 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, @@ -847,6 +1275,16 @@ class Update_Server { } } + /** + * 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 ) { @@ -906,6 +1344,15 @@ class Update_Server { // 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' ) ) { @@ -949,6 +1396,16 @@ class Update_Server { 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'] ) ) { @@ -963,6 +1420,16 @@ class Update_Server { } 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 ), @@ -985,6 +1452,15 @@ class Update_Server { 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'] ) ) { @@ -996,6 +1472,14 @@ class Update_Server { 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 ) ) { @@ -1008,12 +1492,29 @@ class Update_Server { 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 ) ) { @@ -1045,6 +1546,17 @@ class Update_Server { 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 ); @@ -1066,6 +1578,15 @@ class Update_Server { 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 ); @@ -1077,14 +1598,42 @@ class Update_Server { 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 { From 0c3c1ab306ff312650cb83da7f0b8726484d684a Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 20 Mar 2025 22:45:55 +0800 Subject: [PATCH 36/49] Full documentation MVP --- inc/class-upserv.php | 395 +++++++++++++++++- inc/class-utils.php | 75 +++- inc/server/update/class-update-server.php | 66 +++ .../update/class-zip-metadata-parser.php | 258 ++++++++---- inc/table/class-licenses-table.php | 112 +++++ inc/table/class-packages-table.php | 150 ++++++- readme.txt | 5 +- updatepulse-server.php | 2 +- 8 files changed, 958 insertions(+), 105 deletions(-) diff --git a/inc/class-upserv.php b/inc/class-upserv.php index 6a21850..2377181 100644 --- a/inc/class-upserv.php +++ b/inc/class-upserv.php @@ -11,12 +11,41 @@ use Anyape\UpdatePulse\Server\Manager\Data_Manager; use Anyape\UpdatePulse\Server\Manager\Package_Manager; use Anyape\Utils\Utils; +/** + * Main server class for UpdatePulse + * + * @since 1.0.0 + */ class UPServ { + /** + * Class instance + * + * @var UPServ|null + * @since 1.0.0 + */ protected static $instance; + /** + * Default plugin options + * + * @var array + * @since 1.0.0 + */ protected static $default_options; + /** + * Current plugin options + * + * @var array + * @since 1.0.0 + */ protected static $options; + /** + * Constructor + * + * @param boolean $init_hooks Whether to initialize hooks + * @since 1.0.0 + */ public function __construct( $init_hooks = false ) { self::$default_options = array( 'use_vcs' => 0, @@ -79,6 +108,16 @@ class UPServ { * Public methods *******************************************************************/ + /** + * Handle Action Scheduler failed execution + * + * Logs information about failed scheduled actions when debug mode is enabled. + * + * @param int $action_id The ID of the failed action + * @param Exception $exception The exception that was thrown + * @param string $context Additional context information + * @since 1.0.0 + */ public function action_scheduler_failed_execution( $action_id, Exception $exception, $context = '' ) { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { @@ -96,6 +135,13 @@ class UPServ { // WordPress hooks --------------------------------------------- + /** + * Activate plugin + * + * Runs on plugin activation to verify requirements and initialize settings. + * + * @since 1.0.0 + */ public static function activate() { if ( ! version_compare( phpversion(), '8.0', '>=' ) ) { @@ -127,25 +173,70 @@ class UPServ { } } + /** + * Deactivate plugin + * + * Runs on plugin deactivation. + * + * @since 1.0.0 + */ public static function deactivate() { flush_rewrite_rules(); } + /** + * Uninstall plugin + * + * Runs on plugin uninstallation. + * + * @since 1.0.0 + */ public static function uninstall() { require_once UPSERV_PLUGIN_PATH . 'uninstall.php'; } + /** + * Get all plugin options + * + * Retrieves the plugin's options from the database. + * + * @return array Plugin options + * @since 1.0.0 + */ public function get_options() { $options = get_option( 'upserv_options' ); $options = json_decode( $options, true ); $options = $options ? $options : array(); $options = array_merge( self::$default_options, $options ); + /** + * Filter the plugin options. + * + * @param array $options The plugin options + * @return array The filtered options + * @since 1.0.0 + */ return apply_filters( 'upserv_get_options', $options ); } + /** + * Update plugin options + * + * Updates the plugin's options in the database. + * + * @param array $options New options to update + * @return bool Whether the update was successful + * @since 1.0.0 + */ public function update_options( $options ) { $options = array_merge( self::$options, $options ); + /** + * Filter the options before updating. + * + * @param array $options The options to update + * @return array The filtered options + * @since 1.0.0 + */ $options = apply_filters( 'upserv_update_options', $options ); $options = wp_json_encode( $options, @@ -160,6 +251,16 @@ class UPServ { return $result; } + /** + * Get single option value + * + * Retrieves a specific option by its path. + * + * @param string|array $path Option path + * @param mixed $_default Default value if option not found + * @return mixed Option value + * @since 1.0.0 + */ public function get_option( $path, $_default ) { $options = $this->get_options(); $option = Utils::access_nested_array( $options, $path ); @@ -168,9 +269,27 @@ class UPServ { $option = $_default; } + /** + * Filter a specific option value. + * + * @param mixed $option The option value + * @param string|array $path The option path + * @return mixed The filtered option value + * @since 1.0.0 + */ return apply_filters( 'upserv_get_option', $option, $path ); } + /** + * Set option in memory + * + * Sets an option value in memory without saving to database. + * + * @param string|array $path Option path + * @param mixed $value Option value + * @return array Updated options + * @since 1.0.0 + */ public function set_option( $path, $value ) { $options = self::$options; @@ -181,6 +300,16 @@ class UPServ { return self::$options; } + /** + * Update single option + * + * Updates a specific option by its path and saves to database. + * + * @param string|array $path Option path + * @param mixed $value Option value + * @return bool Whether the update was successful + * @since 1.0.0 + */ public function update_option( $path, $value ) { $options = $this->get_options(); @@ -189,6 +318,13 @@ class UPServ { return $this->update_options( $options ); } + /** + * Initialize plugin + * + * Runs during WordPress init hook to set up the plugin. + * + * @since 1.0.0 + */ public function init() { if ( get_transient( 'upserv_flush' ) ) { @@ -207,10 +343,26 @@ class UPServ { } } + /** + * Load text domain + * + * Loads the plugin's translations. + * + * @since 1.0.0 + */ public function load_textdomain() { load_plugin_textdomain( 'updatepulse-server', false, '/languages' ); } + /** + * Register admin styles + * + * Adds stylesheets for the admin interface. + * + * @param array $styles Existing styles + * @return array Modified styles + * @since 1.0.0 + */ public function upserv_admin_styles( $styles ) { $styles['main'] = array( 'path' => UPSERV_PLUGIN_PATH . 'css/admin/main' . upserv_assets_suffix() . '.css', @@ -232,6 +384,15 @@ class UPServ { return $styles; } + /** + * Register admin scripts + * + * Adds JavaScript files for the admin interface. + * + * @param array $scripts Existing scripts + * @return array Modified scripts + * @since 1.0.0 + */ public function upserv_admin_scripts( $scripts ) { $scripts['main'] = array( 'path' => UPSERV_PLUGIN_PATH . 'js/admin/main' . upserv_assets_suffix() . '.js', @@ -246,6 +407,16 @@ class UPServ { return $scripts; } + /** + * Process script localization + * + * Formats localization strings for JavaScript files. + * + * @param array $l10n Localization data + * @param string $script Script name + * @return array Modified localization data + * @since 1.0.0 + */ public function upserv_scripts_l10n( $l10n, $script ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed foreach ( $l10n as $key => $values ) { @@ -260,6 +431,14 @@ class UPServ { return $l10n; } + /** + * Enqueue admin scripts and styles + * + * Loads the necessary assets for admin pages. + * + * @param string $hook Current admin page hook + * @since 1.0.0 + */ public function admin_enqueue_scripts( $hook ) { if ( false !== strpos( $hook, 'page_upserv' ) ) { @@ -268,6 +447,13 @@ class UPServ { } } + /** + * Register main admin menu + * + * Adds the main UpdatePulse menu item to the admin menu. + * + * @since 1.0.0 + */ public function admin_menu() { $page_title = __( 'UpdatePulse', 'updatepulse-server' ); $menu_title = $page_title; @@ -276,6 +462,13 @@ class UPServ { add_menu_page( $page_title, $menu_title, 'manage_options', 'upserv-page', '', $icon ); } + /** + * Register help page in admin menu + * + * Adds the help submenu to the UpdatePulse menu. + * + * @since 1.0.0 + */ public function admin_menu_help() { $function = array( $this, 'help_page' ); $page_title = __( 'UpdatePulse Server - Help', 'updatepulse-server' ); @@ -285,6 +478,15 @@ class UPServ { add_submenu_page( 'upserv-page', $page_title, $menu_title, 'manage_options', $menu_slug, $function ); } + /** + * Add tab links for admin interface + * + * Registers navigation tabs for the admin interface. + * + * @param array $links Existing tab links + * @return array Modified tab links + * @since 1.0.0 + */ public function upserv_admin_tab_links( $links ) { $links['help'] = array( admin_url( 'admin.php?page=upserv-page-help' ), @@ -294,12 +496,31 @@ class UPServ { return $links; } + /** + * Add tab states for admin interface + * + * Sets active states for navigation tabs. + * + * @param array $states Existing tab states + * @param string $page Current page + * @return array Modified tab states + * @since 1.0.0 + */ public function upserv_admin_tab_states( $states, $page ) { $states['help'] = 'upserv-page-help' === $page; return $states; } + /** + * Add plugin action links + * + * Adds custom links to the plugin's entry in the plugins list. + * + * @param array $links Existing plugin action links + * @return array Modified plugin action links + * @since 1.0.0 + */ public function add_action_links( $links ) { $link = array( '' . __( 'Help', 'updatepulse-server' ) . '', @@ -308,10 +529,28 @@ class UPServ { return array_merge( $links, $link ); } + /** + * Set action scheduler retention period + * + * Controls how long scheduled actions are kept in the database. + * + * @return int Retention period in seconds + * @since 1.0.0 + */ public function action_scheduler_retention_period() { return DAY_IN_SECONDS; } + /** + * Modify admin template arguments + * + * Adds or modifies arguments passed to admin templates. + * + * @param array $args Existing template arguments + * @param string $template_name Name of the template + * @return array Modified template arguments + * @since 1.0.0 + */ public function upserv_get_admin_template_args( $args, $template_name ) { if ( preg_match( '/^plugin-.*-page\.php$/', $template_name ) ) { @@ -323,6 +562,14 @@ class UPServ { // Misc. ------------------------------------------------------- + /** + * Get class instance + * + * Retrieves or creates the singleton instance of this class. + * + * @return UPServ The class instance + * @since 1.0.0 + */ public static function get_instance() { if ( ! isset( self::$instance ) ) { @@ -332,20 +579,47 @@ class UPServ { return self::$instance; } + /** + * Locate template file + * + * Finds a template file in the theme or plugin directories. + * + * @param string $template_name Template name + * @param bool $load Whether to load the template + * @param bool $required_once Whether to use require_once or require + * @return string Template path + * @since 1.0.0 + */ public static function locate_template( $template_name, $load = false, $required_once = true ) { - $name = str_replace( 'templates/', '', $template_name ); - $paths = array( + $name = str_replace( 'templates/', '', $template_name ); + $paths = array( 'plugins/updatepulse-server/templates/' . $name, 'plugins/updatepulse-server/' . $name, 'updatepulse-server/templates/' . $name, 'updatepulse-server/' . $name, ); + /** + * Filter the paths where templates can be located. + * + * @param array $paths Array of template paths + * @return array The filtered paths + * @since 1.0.0 + */ $template = locate_template( apply_filters( 'upserv_locate_template_paths', $paths ) ); if ( empty( $template ) ) { $template = UPSERV_PLUGIN_PATH . 'inc/templates/' . $template_name; } + /** + * Filter the located template. + * + * @param string $template The path to the template + * @param string $template_name The template name + * @param string $template_path The template path + * @return string The filtered template path + * @since 1.0.0 + */ $template = apply_filters( 'upserv_locate_template', $template, @@ -360,7 +634,27 @@ class UPServ { return $template; } + /** + * Locate admin template file + * + * Finds an admin template file in the plugin directory. + * + * @param string $template_name Template name + * @param bool $load Whether to load the template + * @param bool $required_once Whether to use require_once or require + * @return string Template path + * @since 1.0.0 + */ public static function locate_admin_template( $template_name, $load = false, $required_once = true ) { + /** + * Filter the admin template location. + * + * @param string $template The path to the template + * @param string $template_name The template name + * @param string $template_path The template path + * @return string The filtered template path + * @since 1.0.0 + */ $template = apply_filters( 'upserv_locate_admin_template', UPSERV_PLUGIN_PATH . 'inc/templates/admin/' . $template_name, @@ -375,6 +669,13 @@ class UPServ { return $template; } + /** + * Display MU plugin setup failure notice + * + * Shows admin notice when MU plugin couldn't be installed. + * + * @since 1.0.0 + */ public function setup_mu_plugin_failure_notice() { $class = 'notice notice-error'; $message = sprintf( @@ -387,6 +688,13 @@ class UPServ { printf( '

%2$s

', $class, $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * Display MU plugin setup success notice + * + * Shows admin notice when MU plugin was successfully installed. + * + * @since 1.0.0 + */ public function setup_mu_plugin_success_notice() { $class = 'notice notice-info is-dismissible'; $message = sprintf( @@ -398,6 +706,14 @@ class UPServ { printf( '

%2$s

', $class, $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + /** + * Display settings header + * + * Renders the header for settings pages with notices. + * + * @param string|array $notice Optional notice to display + * @since 1.0.0 + */ public function display_settings_header( $notice ) { echo '

' . esc_html__( 'UpdatePulse Server', 'updatepulse-server' ) . '

'; @@ -429,6 +745,13 @@ class UPServ { $this->display_tabs(); } + /** + * Render help page + * + * Displays the plugin's help documentation. + * + * @since 1.0.0 + */ public function help_page() { if ( ! current_user_can( 'manage_options' ) ) { @@ -453,6 +776,13 @@ class UPServ { * Protected methods *******************************************************************/ + /** + * Display navigation tabs + * + * Renders the tab navigation for admin pages. + * + * @since 1.0.0 + */ protected function display_tabs() { $states = $this->get_tab_states(); $state = array_filter( $states ); @@ -463,6 +793,13 @@ class UPServ { $state = array_keys( $state ); $state = reset( $state ); + /** + * Filter the admin tab links. + * + * @param array $links The existing tab links + * @return array The modified tab links + * @since 1.0.0 + */ $links = apply_filters( 'upserv_admin_tab_links', array() ); upserv_get_admin_template( @@ -475,20 +812,51 @@ class UPServ { ); } + /** + * Get tab states + * + * Determines which tab is currently active. + * + * @return array Tab states + * @since 1.0.0 + */ protected function get_tab_states() { $page = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'page' ) ) ); $states = array(); if ( 0 === strpos( $page, 'upserv-page' ) ) { + /** + * Filter the admin tab states. + * + * @param array $states The existing tab states + * @param string $page The current page + * @return array The modified tab states + * @since 1.0.0 + */ $states = apply_filters( 'upserv_admin_tab_states', $states, $page ); } return $states; } + /** + * Enqueue styles + * + * Loads stylesheets for the admin interface. + * + * @param array $styles Styles to enqueue + * @return array Enqueued styles + * @since 1.0.0 + */ protected function enqueue_styles( $styles ) { - $filter = 'upserv_admin_styles'; - $styles = apply_filters( $filter, $styles ); + /** + * Filter the admin styles to be enqueued. + * + * @param array $styles Array of styles to be enqueued + * @return array Modified array of styles + * @since 1.0.0 + */ + $styles = apply_filters( 'upserv_admin_styles', $styles ); if ( ! empty( $styles ) ) { @@ -516,9 +884,24 @@ class UPServ { return $styles; } + /** + * Enqueue scripts + * + * Loads JavaScript files for the admin interface. + * + * @param array $scripts Scripts to enqueue + * @return array Enqueued scripts + * @since 1.0.0 + */ protected function enqueue_scripts( $scripts ) { - $filter = 'upserv_admin_scripts'; - $scripts = apply_filters( $filter, $scripts ); + /** + * Filter the admin scripts to be enqueued. + * + * @param array $scripts Array of scripts to be enqueued + * @return array Modified array of scripts + * @since 1.0.0 + */ + $scripts = apply_filters( 'upserv_admin_scripts', $scripts ); if ( ! empty( $scripts ) ) { diff --git a/inc/class-utils.php b/inc/class-utils.php index 67f3b73..c84f04d 100644 --- a/inc/class-utils.php +++ b/inc/class-utils.php @@ -10,15 +10,26 @@ if ( ! defined( 'ABSPATH' ) ) { * Class Utils * * @package Anyape\Utils + * @since 1.0.0 */ class Utils { - // JSON options + /** + * JSON encoding options + * + * @var int + * @since 1.0.0 + */ const JSON_OPTIONS = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE; /** - * @param string $message - * @param string $prefix + * Log a message to PHP error log + * + * Adds class/method context information to the log message. + * + * @param string $message Message to log + * @param string $prefix Optional prefix for the log message + * @since 1.0.0 */ public static function php_log( $message = '', $prefix = '' ) { $prefix = $prefix ? ' ' . $prefix . ' => ' : ' => '; @@ -33,10 +44,14 @@ class Utils { } /** - * @param string $ip - * @param string $range + * Check if IP address is within CIDR range * - * @return bool + * Validates whether a given IP address falls within the specified CIDR range. + * + * @param string $ip IP address to check + * @param string $range CIDR range notation (e.g., 192.168.1.0/24) + * @return bool True if IP is in range, false otherwise + * @since 1.0.0 */ public static function cidr_match( $ip, $range ) { list ( $subnet, $bits ) = explode( '/', $range ); @@ -54,12 +69,16 @@ class Utils { } /** - * @param array $_array - * @param string $path - * @param null $value - * @param bool $update + * Access or update nested array using path notation * - * @return mixed|null + * Gets or sets a value in a nested array using a path string with / as separator. + * + * @param array $_array Reference to the array to access + * @param string $path Path notation to the nested element (e.g., 'parent/child/item') + * @param mixed $value Optional value to set if updating + * @param bool $update Whether to update the array (true) or just read (false) + * @return mixed|null Retrieved value or null if path doesn't exist + * @since 1.0.0 */ public static function access_nested_array( &$_array, $path, $value = null, $update = false ) { $keys = explode( '/', $path ); @@ -87,10 +106,13 @@ class Utils { } /** - * @param string $path - * @param string $regex + * Check if URL subpath matches a regex pattern * - * @return int|null + * Tests if the first segment of the current request URI matches the provided regex. + * + * @param string $regex Regular expression to match against the first path segment + * @return int|null 1 if match found, 0 if no match, null if host couldn't be determined + * @since 1.0.0 */ public static function is_url_subpath_match( $regex ) { $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : false; @@ -111,10 +133,12 @@ class Utils { } /** - * @param string $path - * @param string $regex + * Get time elapsed since request start * - * @return int|null + * Calculates the time elapsed since the request started in seconds. + * + * @return string|null Time elapsed in seconds with 3 decimal precision, or null if request time not available + * @since 1.0.0 */ public static function get_time_elapsed() { @@ -132,10 +156,12 @@ class Utils { } /** - * @param string $path - * @param string $regex + * Get remote IP address * - * @return int|null + * Safely retrieves the remote IP address of the client. + * + * @return string IP address of the client or '0.0.0.0' if not available or invalid + * @since 1.0.0 */ public static function get_remote_ip() { @@ -152,6 +178,15 @@ class Utils { return $ip; } + /** + * Get human-readable status string + * + * Converts a status code to a localized human-readable string. + * + * @param string $status Status code to convert + * @return string Localized human-readable status string + * @since 1.0.0 + */ public static function get_status_string( $status ) { switch ( $status ) { case 'pending': diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php index af3cb4c..d1f1675 100644 --- a/inc/server/update/class-update-server.php +++ b/inc/server/update/class-update-server.php @@ -288,6 +288,13 @@ class Update_Server { } } + /** + * 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; @@ -314,6 +321,13 @@ class Update_Server { */ $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; @@ -365,6 +379,13 @@ class Update_Server { ) ) { $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; @@ -375,6 +396,13 @@ class Update_Server { $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( @@ -385,6 +413,13 @@ class Update_Server { ); $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, @@ -429,6 +464,11 @@ class Update_Server { * @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; @@ -491,6 +531,13 @@ class Update_Server { $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; @@ -552,6 +599,13 @@ class Update_Server { $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 ); @@ -709,6 +763,11 @@ class Update_Server { * @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 ); /** @@ -846,6 +905,13 @@ class Update_Server { } 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 ); } diff --git a/inc/server/update/class-zip-metadata-parser.php b/inc/server/update/class-zip-metadata-parser.php index fbab5d0..74f4c52 100644 --- a/inc/server/update/class-zip-metadata-parser.php +++ b/inc/server/update/class-zip-metadata-parser.php @@ -14,14 +14,24 @@ use Anyape\UpdatePulse\Package_Parser\Parser; class Zip_Metadata_Parser { /** - * @var int $cache_time How long the package metadata should be cached in seconds. - * Defaults to 1 week ( 7 * 24 * 60 * 60 ). - */ + * Cache time + * + * How long the package metadata should be cached in seconds. + * Defaults to 1 week ( 7 * 24 * 60 * 60 ). + * + * @var int + * @since 1.0.0 + */ public static $cache_time = 604800; /** - * @var array Package PHP header mapping, i.e. which tags to add to the metadata under which array key - */ + * Header map + * + * Package PHP header mapping, i.e. which tags to add to the metadata under which array key. + * + * @var array + * @since 1.0.0 + */ protected $header_map = array( 'Name' => 'name', 'Version' => 'version', @@ -36,49 +46,76 @@ class Zip_Metadata_Parser { 'Depends' => 'depends', 'Provides' => 'provides', ); - /** - * @var array Plugin readme file mapping, i.e. which tags to add to the metadata - */ + * Readme map + * + * Plugin readme file mapping, i.e. which tags to add to the metadata. + * + * @var array + * @since 1.0.0 + */ protected $readme_map = array( 'requires', 'tested', 'requires_php', ); - /** - * @var array Package info as retrieved by the parser - */ + * Package info + * + * Package info as retrieved by the parser. + * + * @var array + * @since 1.0.0 + */ protected $package_info; - /** - * @var string Path to the Zip archive that contains the package. - */ + * Filename + * + * Path to the Zip archive that contains the package. + * + * @var string + * @since 1.0.0 + */ protected $filename; - /** - * @var string Package slug. - */ + * Slug + * + * Package slug. + * + * @var string + * @since 1.0.0 + */ protected $slug; - /** - * @var Cache object. - */ + * Cache + * + * Cache object. + * + * @var object + * @since 1.0.0 + */ protected $cache; - /** - * @var array Package metadata in a format suitable for the update checker. - */ + * Metadata + * + * Package metadata in a format suitable for the update checker. + * + * @var array + * @since 1.0.0 + */ protected $metadata; /** - * Get the metadata from a zip file. - * - * @param string $slug - * @param string $filename - * @param $cache - */ + * Constructor + * + * Get the metadata from a zip file. + * + * @param string $slug Package slug. + * @param string $filename Path to the Zip archive. + * @param object $cache Cache object. + * @since 1.0.0 + */ public function __construct( $slug, $filename, $cache = null ) { $this->slug = $slug; $this->filename = $filename; @@ -88,8 +125,15 @@ class Zip_Metadata_Parser { } /** - * Build the cache key (cache filename) for a file - */ + * Build cache key + * + * Build the cache key (cache filename) for a file. + * + * @param string $slug Package slug. + * @param string $filename Path to the Zip archive. + * @return string The cache key. + * @since 1.0.0 + */ public static function build_cache_key( $slug, $filename ) { $cache_key = $slug . '-b64-'; @@ -97,6 +141,15 @@ class Zip_Metadata_Parser { $cache_key .= md5( $filename . '|' . filesize( $filename ) . '|' . filemtime( $filename ) ); } + /** + * Filter the cache key used for storing package metadata. + * + * @param string $cache_key The generated cache key for the package. + * @param string $slug The package slug. + * @param string $filename The path to the Zip archive. + * @return string The filtered cache key. + * @since 1.0.0 + */ return apply_filters( 'upserv_zip_metadata_parser_cache_key', $cache_key, @@ -106,20 +159,27 @@ class Zip_Metadata_Parser { } /** - * Get metadata. - * - * @return array - */ + * Get metadata + * + * Get the package metadata. + * + * @return array Package metadata. + * @since 1.0.0 + */ public function get() { return $this->metadata; } /** - * Load metadata information from a cache or create it. - * - * We'll try to load processed metadata from the cache first (if available), and if that - * fails we'll extract package details from the specified Zip file. - */ + * Set metadata + * + * Load metadata information from a cache or create it. + * + * We'll try to load processed metadata from the cache first (if available), and if that + * fails we'll extract package details from the specified Zip file. + * + * @since 1.0.0 + */ protected function set_metadata() { $cache_key = self::build_cache_key( $this->slug, $this->filename ); @@ -151,11 +211,14 @@ class Zip_Metadata_Parser { } /** - * Extract package headers and readme contents from a ZIP file and convert them - * into a structure compatible with the custom update checker. - * - * @throws Invalid_Package_Exception if the input file can't be parsed as a package. - */ + * Extract metadata + * + * Extract package headers and readme contents from a ZIP file and convert them + * into a structure compatible with the custom update checker. + * + * @throws Invalid_Package_Exception if the input file can't be parsed as a package. + * @since 1.0.0 + */ protected function extract_metadata() { $this->package_info = Parser::parse_package( $this->filename, true ); @@ -177,8 +240,12 @@ class Zip_Metadata_Parser { } /** - * Extract relevant metadata from the package header information - */ + * Set info from header + * + * Extract relevant metadata from the package header information. + * + * @since 1.0.0 + */ protected function set_info_from_header() { if ( isset( $this->package_info['header'] ) && ! empty( $this->package_info['header'] ) ) { @@ -188,8 +255,12 @@ class Zip_Metadata_Parser { } /** - * Extract relevant metadata from the plugin readme - */ + * Set info from readme + * + * Extract relevant metadata from the plugin readme. + * + * @since 1.0.0 + */ protected function set_info_from_readme() { if ( ! empty( $this->package_info['readme'] ) ) { @@ -202,15 +273,18 @@ class Zip_Metadata_Parser { } /** - * Extract selected metadata from the retrieved package info - * - * @see http://codex.wordpress.org/File_Header - * @see https://wordpress.org/plugins/about/readme.txt - * - * @param array $input The package info sub-array to use to retrieve the info from - * @param array $map The key mapping for that sub-array where the key is the key as used in the - * input array and the value is the key to use for the output array - */ + * Set mapped fields + * + * Extract selected metadata from the retrieved package info. + * + * @see http://codex.wordpress.org/File_Header + * @see https://wordpress.org/plugins/about/readme.txt + * + * @param array $input The package info sub-array to use to retrieve the info from. + * @param array $map The key mapping for that sub-array where the key is the key as used in the + * input array and the value is the key to use for the output array. + * @since 1.0.0 + */ protected function set_mapped_fields( $input, $map ) { foreach ( $map as $field_key => $meta_key ) { @@ -222,12 +296,16 @@ class Zip_Metadata_Parser { } /** - * Determine the details url for themes - * - * Theme metadata should include a "details_url" that specifies the page to display - * when the user clicks "View version x.y.z details". If the developer didn't provide - * it by setting the "Details URI" header, we'll default to the theme homepage ( "Theme URI" ). - */ + * Set theme details URL + * + * Determine the details url for themes. + * + * Theme metadata should include a "details_url" that specifies the page to display + * when the user clicks "View version x.y.z details". If the developer didn't provide + * it by setting the "Details URI" header, we'll default to the theme homepage ( "Theme URI" ). + * + * @since 1.0.0 + */ protected function set_theme_details_url() { if ( @@ -239,10 +317,13 @@ class Zip_Metadata_Parser { } /** - * Extract the texual information sections from a readme file - * - * @see https://wordpress.org/plugins/about/readme.txt - */ + * Set readme sections + * + * Extract the texual information sections from a readme file. + * + * @see https://wordpress.org/plugins/about/readme.txt + * @since 1.0.0 + */ protected function set_readme_sections() { if ( @@ -263,10 +344,13 @@ class Zip_Metadata_Parser { } /** - * Extract the upgrade notice for the current version from a readme file - * - * @see https://wordpress.org/plugins/about/readme.txt - */ + * Set readme upgrade notice + * + * Extract the upgrade notice for the current version from a readme file. + * + * @see https://wordpress.org/plugins/about/readme.txt + * @since 1.0.0 + */ protected function set_readme_upgrade_notice() { //Check if we have an upgrade notice for this version @@ -282,8 +366,12 @@ class Zip_Metadata_Parser { } /** - * Add last update date to the metadata ; this is tied to the version - */ + * Set last update date + * + * Add last update date to the metadata; this is tied to the version. + * + * @since 1.0.0 + */ protected function set_last_update_date() { if ( isset( $this->metadata['last_updated'] ) ) { @@ -309,10 +397,24 @@ class Zip_Metadata_Parser { $this->metadata['last_updated'] = $meta['version_time']; } + /** + * Set type + * + * Set the package type in the metadata. + * + * @since 1.0.0 + */ protected function set_type() { $this->metadata['type'] = $this->package_info['type']; } + /** + * Set slug + * + * Set the package slug in the metadata. + * + * @since 1.0.0 + */ protected function set_slug() { if ( 'plugin' === $this->package_info['type'] ) { @@ -327,8 +429,12 @@ class Zip_Metadata_Parser { } /** - * Extract icons and banners info for plugins - */ + * Set info from assets + * + * Extract icons and banners info for plugins. + * + * @since 1.0.0 + */ protected function set_info_from_assets() { if ( ! empty( $this->package_info['extra'] ) ) { diff --git a/inc/table/class-licenses-table.php b/inc/table/class-licenses-table.php index ded7149..3c76dee 100644 --- a/inc/table/class-licenses-table.php +++ b/inc/table/class-licenses-table.php @@ -9,13 +9,43 @@ if ( ! defined( 'ABSPATH' ) ) { use WP_List_Table; use Anyape\Utils\Utils; +/** + * Licenses table class + * + * @since 1.0.0 + */ class Licenses_Table extends WP_List_Table { + /** + * Bulk action error + * + * @var mixed + * @since 1.0.0 + */ public $bulk_action_error; + /** + * Nonce action name + * + * @var string + * @since 1.0.0 + */ public $nonce_action; + /** + * Table rows data + * + * @var array + * @since 1.0.0 + */ protected $rows; + /** + * Constructor + * + * Sets up the table properties and hooks + * + * @since 1.0.0 + */ public function __construct() { parent::__construct( array( @@ -34,6 +64,14 @@ class Licenses_Table extends WP_List_Table { // Overrides --------------------------------------------------- + /** + * Get table columns + * + * Define the columns for the licenses table + * + * @return array The table columns + * @since 1.0.0 + */ public function get_columns() { return array( 'cb' => '', @@ -48,10 +86,28 @@ class Licenses_Table extends WP_List_Table { ); } + /** + * Default column rendering + * + * Default handler for displaying column data + * + * @param array $item The row item + * @param string $column_name The column name + * @return mixed The column value + * @since 1.0.0 + */ public function column_default( $item, $column_name ) { return $item[ $column_name ]; } + /** + * Get sortable columns + * + * Define which columns can be sorted + * + * @return array The sortable columns + * @since 1.0.0 + */ public function get_sortable_columns() { return array( 'col_status' => array( 'status', false ), @@ -64,6 +120,13 @@ class Licenses_Table extends WP_List_Table { ); } + /** + * Prepare table items + * + * Query the database and set up the items for display + * + * @since 1.0.0 + */ public function prepare_items() { global $wpdb; @@ -174,6 +237,13 @@ class Licenses_Table extends WP_List_Table { $this->items = $items; } + /** + * Display table rows + * + * Output the HTML for each row in the table + * + * @since 1.0.0 + */ public function display_rows() { $records = $this->items; $table = $this; @@ -214,6 +284,14 @@ class Licenses_Table extends WP_List_Table { // Misc. ------------------------------------------------------- + /** + * Set table rows + * + * Set the row data for the table + * + * @param array $rows The rows data + * @since 1.0.0 + */ public function set_rows( $rows ) { $this->rows = $rows; } @@ -224,6 +302,16 @@ class Licenses_Table extends WP_List_Table { // Overrides --------------------------------------------------- + /** + * Generate row actions + * + * Create action links for each row + * + * @param array $actions The actions array + * @param bool $always_visible Whether actions should be always visible + * @return string HTML for the row actions + * @since 1.0.0 + */ protected function row_actions( $actions, $always_visible = false ) { $action_count = count( $actions ); $i = 0; @@ -248,6 +336,14 @@ class Licenses_Table extends WP_List_Table { return $out; } + /** + * Display extra tablenav + * + * Add additional controls above or below the table + * + * @param string $which The location ('top' or 'bottom') + * @since 1.0.0 + */ protected function extra_tablenav( $which ) { if ( 'bottom' === $which ) { @@ -255,6 +351,14 @@ class Licenses_Table extends WP_List_Table { } } + /** + * Get bulk actions + * + * Define available bulk actions for the table + * + * @return array The available bulk actions + * @since 1.0.0 + */ protected function get_bulk_actions() { $actions = array( 'pending' => __( 'Set to Pending', 'updatepulse-server' ), @@ -268,6 +372,14 @@ class Licenses_Table extends WP_List_Table { return $actions; } + /** + * Get table classes + * + * Define CSS classes for the table + * + * @return array The table CSS classes + * @since 1.0.0 + */ protected function get_table_classes() { $mode = get_user_setting( 'posts_list_mode', 'list' ); diff --git a/inc/table/class-packages-table.php b/inc/table/class-packages-table.php index b2c5689..0ebe2bd 100644 --- a/inc/table/class-packages-table.php +++ b/inc/table/class-packages-table.php @@ -9,14 +9,51 @@ if ( ! defined( 'ABSPATH' ) ) { use WP_List_Table; use DateTimeZone; +/** + * Packages Table class + * + * Manages the display of packages in the admin area + * + * @since 1.0.0 + */ class Packages_Table extends WP_List_Table { + /** + * Bulk action error message + * + * @var string|null + * @since 1.0.0 + */ public $bulk_action_error; + /** + * Nonce action name + * + * @var string + * @since 1.0.0 + */ public $nonce_action; + /** + * Table rows data + * + * @var array + * @since 1.0.0 + */ protected $rows; + /** + * Package manager instance + * + * @var object + * @since 1.0.0 + */ protected $package_manager; + /** + * Constructor + * + * @param object $package_manager The package manager instance + * @since 1.0.0 + */ public function __construct( $package_manager ) { parent::__construct( array( @@ -36,7 +73,22 @@ class Packages_Table extends WP_List_Table { // Overrides --------------------------------------------------- + /** + * Get table columns + * + * Define the columns shown in the packages table. + * + * @return array Table columns + * @since 1.0.0 + */ public function get_columns() { + /** + * Filter the columns shown in the packages table. + * + * @param array $columns The default columns for the packages table + * @return array The filtered columns + * @since 1.0.0 + */ $columns = apply_filters( 'upserv_packages_table_columns', array( @@ -54,11 +106,36 @@ class Packages_Table extends WP_List_Table { return $columns; } + /** + * Default column renderer + * + * Default handler for columns without specific renderers. + * + * @param array $item The current row item + * @param string $column_name The current column name + * @return mixed Column content + * @since 1.0.0 + */ public function column_default( $item, $column_name ) { return $item[ $column_name ]; } + /** + * Get sortable columns + * + * Define which columns can be sorted in the table. + * + * @return array Sortable columns configuration + * @since 1.0.0 + */ public function get_sortable_columns() { + /** + * Filter the sortable columns in the packages table. + * + * @param array $columns The default sortable columns + * @return array The filtered sortable columns + * @since 1.0.0 + */ $columns = apply_filters( 'upserv_packages_table_sortable_columns', array( @@ -75,6 +152,13 @@ class Packages_Table extends WP_List_Table { return $columns; } + /** + * Prepare table items + * + * Process data for table display including pagination. + * + * @since 1.0.0 + */ public function prepare_items() { $total_items = count( $this->rows ); $offset = 0; @@ -111,7 +195,13 @@ class Packages_Table extends WP_List_Table { uasort( $this->items, array( &$this, 'uasort_reorder' ) ); } - + /** + * Display table rows + * + * Render the rows of the packages table. + * + * @since 1.0.0 + */ public function display_rows() { $records = $this->items; $table = $this; @@ -206,10 +296,28 @@ class Packages_Table extends WP_List_Table { // Misc. ------------------------------------------------------- + /** + * Set table rows + * + * Set the rows data for the table. + * + * @param array $rows Table rows data + * @since 1.0.0 + */ public function set_rows( $rows ) { $this->rows = $rows; } + /** + * Custom sorting function + * + * Sort table items based on request parameters. + * + * @param array $a First item to compare + * @param array $b Second item to compare + * @return int Comparison result + * @since 1.0.0 + */ public function uasort_reorder( $a, $b ) { $order_by = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : 'name'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'asc'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended @@ -242,6 +350,14 @@ class Packages_Table extends WP_List_Table { // Overrides --------------------------------------------------- + /** + * Display extra table navigation + * + * Add additional controls above or below the table. + * + * @param string $which Position ('top' or 'bottom') + * @since 1.0.0 + */ protected function extra_tablenav( $which ) { if ( 'top' === $which ) { @@ -258,6 +374,14 @@ class Packages_Table extends WP_List_Table { } } + /** + * Get table CSS classes + * + * Define the CSS classes for the table. + * + * @return array Table CSS classes + * @since 1.0.0 + */ protected function get_table_classes() { $mode = get_user_setting( 'posts_list_mode', 'list' ); $mode_class = esc_attr( 'table-view-' . $mode ); @@ -265,7 +389,22 @@ class Packages_Table extends WP_List_Table { return array( 'widefat', 'striped', $mode_class, $this->_args['plural'] ); } + /** + * Get bulk actions + * + * Define available bulk actions for the table. + * + * @return array Bulk actions + * @since 1.0.0 + */ protected function get_bulk_actions() { + /** + * Filter the bulk actions available in the packages table. + * + * @param array $actions The default bulk actions + * @return array The filtered bulk actions + * @since 1.0.0 + */ $actions = apply_filters( 'upserv_packages_table_bulk_actions', array( @@ -277,6 +416,15 @@ class Packages_Table extends WP_List_Table { return $actions; } + /** + * Get VCS icon class + * + * Get the appropriate icon class for a VCS provider. + * + * @param array $vcs_config VCS configuration + * @return string CSS class for the VCS icon + * @since 1.0.0 + */ protected function get_vcs_class( $vcs_config ) { switch ( $vcs_config['type'] ) { diff --git a/readme.txt b/readme.txt index 4e90486..00a9ff0 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.6 +Stable tag: 1.0.7 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -119,6 +119,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.7 = +* Full documentation of all classes and functions + = 1.0.6 = * Fix webhook payload handling (thanks @eHtmlu on github) * Fix webhook payload scheduling (thanks @BabaYaga0179 on github) diff --git a/updatepulse-server.php b/updatepulse-server.php index ec7fbd5..c8ffba8 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.6 + * Version: 1.0.7 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From 4bb83b483723b4a291e621a38ad0c8303d2f5c62 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:14:29 +0800 Subject: [PATCH 37/49] Fix scheduled mode package overrides --- inc/manager/class-package-manager.php | 47 +++++++++++++------- inc/manager/class-remote-sources-manager.php | 2 +- inc/server/update/class-update-server.php | 2 +- readme.txt | 14 +++++- updatepulse-server.php | 2 +- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/inc/manager/class-package-manager.php b/inc/manager/class-package-manager.php index fdc28fe..b8b686a 100644 --- a/inc/manager/class-package-manager.php +++ b/inc/manager/class-package-manager.php @@ -18,6 +18,7 @@ use Anyape\UpdatePulse\Server\Server\Update\Package; use Anyape\UpdatePulse\Server\Manager\Data_Manager; use Anyape\UpdatePulse\Server\API\Package_API; use Anyape\UpdatePulse\Server\Table\Packages_Table; +use Anyape\UpdatePulse\Server\Scheduler\Scheduler; use Anyape\Utils\Utils; /** @@ -445,6 +446,25 @@ class Package_Manager { upserv_set_package_metadata( $slug, $meta ); $result = upserv_download_remote_package( $slug, null ); + + if ( wp_cache_get( 'upserv_download_remote_package_aborted', 'updatepulse-server' ) ) { + $vcs_config = upserv_get_package_vcs_config( $slug ); + $error = isset( $vcs_config['filter_packages'] ) && $vcs_config['filter_packages'] ? + new WP_Error( + __METHOD__, + __( 'Error - could not get remote package. The package was filtered out because it is not linked to this server.', 'updatepulse-server' ) + ) : + new WP_Error( + __METHOD__, + __( 'Error - could not get remote package. The package was found and is valid, but the download was aborted. Please check the package is satisfying the requirements for this server.', 'updatepulse-server' ) + ); + + wp_cache_delete( 'upserv_download_remote_package_aborted', 'updatepulse-server' ); + } + + if ( ! $result || isset( $result['abort_request'] ) ) { + upserv_set_package_metadata( $slug, null ); + } } else { $error = new WP_Error( __METHOD__, @@ -458,21 +478,6 @@ class Package_Manager { ); } - if ( wp_cache_get( 'upserv_download_remote_package_aborted', 'updatepulse-server' ) ) { - $vcs_config = upserv_get_package_vcs_config( $slug ); - $error = isset( $vcs_config['filter_packages'] ) && $vcs_config['filter_packages'] ? - new WP_Error( - __METHOD__, - __( 'Error - could not get remote package. The package was filtered out because it is not linked to this server.', 'updatepulse-server' ) - ) : - new WP_Error( - __METHOD__, - __( 'Error - could not get remote package. The package was found and is valid, but the download was aborted. Please check the package is satisfying the requirements for this server.', 'updatepulse-server' ) - ); - - wp_cache_delete( 'upserv_download_remote_package_aborted', 'updatepulse-server' ); - } - /** * Fired after a package has been registered from a VCS. * @@ -989,7 +994,19 @@ class Package_Manager { do_action( 'upserv_package_manager_deleted_package', $slug, $result ); if ( $result ) { + $scheduled_hook = 'upserv_check_remote_' . $slug; + upserv_unwhitelist_package( $slug ); + Scheduler::get_instance()->unschedule_all_actions( $scheduled_hook ); + + /** + * Fired after a remote check schedule event has been unscheduled for a package. + * Fired during client update API request. + * + * @param string $package_slug The slug of the package for which a remote check event has been unscheduled + * @param string $scheduled_hook The remote check event hook that has been unscheduled + */ + do_action( 'upserv_cleared_check_remote_schedule', $slug, $scheduled_hook ); $deleted_package_slugs[] = $slug; diff --git a/inc/manager/class-remote-sources-manager.php b/inc/manager/class-remote-sources-manager.php index bd93012..e821bdd 100644 --- a/inc/manager/class-remote-sources-manager.php +++ b/inc/manager/class-remote-sources-manager.php @@ -885,7 +885,7 @@ class Remote_Sources_Manager { } } - if ( empty( $slugs ) ) { + if ( ! empty( $slugs ) ) { foreach ( $slugs as $idx => $slug ) { $meta = upserv_get_package_metadata( $slug ); diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php index d1f1675..271eb1b 100644 --- a/inc/server/update/class-update-server.php +++ b/inc/server/update/class-update-server.php @@ -517,7 +517,7 @@ class Update_Server { $remote_info = $this->update_checker->request_info(); - if ( $remote_info && ! is_wp_error( $remote_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( diff --git a/readme.txt b/readme.txt index 00a9ff0..bebd86e 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.7 +Stable tag: 1.0.8 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -61,6 +61,15 @@ Each **bug** report will be addressed in a timely manner if properly documented **Troubleshooting involving 3rd-party plugins or themes will not be addressed on the WordPress support forum.** +== Upgrade Notice == + += 1.0.8 = + +For installations using VCS in schedule mode (as opposed to webhook mode): +- delete all packages and re-register them +- remove any remaining `json` files from `wp-content/uploads/updatepulse-server/metadata` folder +- use the "Force Clear & Reschedule" button in the VCS settings + == FAQ == = How do I use UpdatePulse Server? = @@ -119,6 +128,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.8 = +* Fix scheduled mode package overrides. After update, if using this mode: delete all packages and re-register them ; remove any remaining `json` files from `wp-content/uploads/updatepulse-server/metadata` folder ; use the "Force Clear & Reschedule" button in the VCS settings + = 1.0.7 = * Full documentation of all classes and functions diff --git a/updatepulse-server.php b/updatepulse-server.php index c8ffba8..67ecd0b 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.7 + * Version: 1.0.8 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From 2589b5071b180b5f7905b08bd64516419c74b274 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:04:36 +0800 Subject: [PATCH 38/49] Fix VCS candidates with webhook mode --- inc/api/class-webhook-api.php | 1 - readme.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/api/class-webhook-api.php b/inc/api/class-webhook-api.php index 9392ac4..4a4522a 100644 --- a/inc/api/class-webhook-api.php +++ b/inc/api/class-webhook-api.php @@ -424,7 +424,6 @@ class Webhook_API { if ( 0 === strpos( $config['url'], trailingslashit( $url ) ) ) { $vcs_candidates[] = $config; - $vcs_candidates[] = $config; } } } diff --git a/readme.txt b/readme.txt index bebd86e..3aa5e28 100644 --- a/readme.txt +++ b/readme.txt @@ -130,6 +130,7 @@ This section describes how to install the plugin and get it working. = 1.0.8 = * Fix scheduled mode package overrides. After update, if using this mode: delete all packages and re-register them ; remove any remaining `json` files from `wp-content/uploads/updatepulse-server/metadata` folder ; use the "Force Clear & Reschedule" button in the VCS settings +* Fix VCS candidates with webhook mode = 1.0.7 = * Full documentation of all classes and functions From 49adf7f6b4aaba60b60e01ae3e4d7c2bd92b3e74 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Sun, 13 Apr 2025 13:44:15 +0800 Subject: [PATCH 39/49] remove package metadata files when deleting packages ; make sure to reinitialise the update checker to avoid slug conflicts --- inc/manager/class-package-manager.php | 1 + inc/server/update/class-update-server.php | 2 +- readme.txt | 8 ++++++-- updatepulse-server.php | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/inc/manager/class-package-manager.php b/inc/manager/class-package-manager.php index b8b686a..5fc4d8c 100644 --- a/inc/manager/class-package-manager.php +++ b/inc/manager/class-package-manager.php @@ -997,6 +997,7 @@ class Package_Manager { $scheduled_hook = 'upserv_check_remote_' . $slug; upserv_unwhitelist_package( $slug ); + upserv_set_package_metadata( $slug, null ); Scheduler::get_instance()->unschedule_all_actions( $scheduled_hook ); /** diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php index 271eb1b..ace147e 100644 --- a/inc/server/update/class-update-server.php +++ b/inc/server/update/class-update-server.php @@ -1315,7 +1315,7 @@ class Update_Server { $this->self_hosted ); - if ( $this->update_checker ) { + if ( $this->update_checker && $this->update_checker->slug === $slug ) { return; } diff --git a/readme.txt b/readme.txt index 3aa5e28..3c3bfe9 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.8 +Stable tag: 1.0.9 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -63,7 +63,7 @@ Each **bug** report will be addressed in a timely manner if properly documented == Upgrade Notice == -= 1.0.8 = += 1.0.9 = For installations using VCS in schedule mode (as opposed to webhook mode): - delete all packages and re-register them @@ -128,6 +128,10 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.9 = +* Schedule mode: remove package metadata files when deleting packages +* Schedule mode: make sure to reinitialise the update checker to avoid slug conflicts + = 1.0.8 = * Fix scheduled mode package overrides. After update, if using this mode: delete all packages and re-register them ; remove any remaining `json` files from `wp-content/uploads/updatepulse-server/metadata` folder ; use the "Force Clear & Reschedule" button in the VCS settings * Fix VCS candidates with webhook mode diff --git a/updatepulse-server.php b/updatepulse-server.php index 67ecd0b..71c7a02 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.8 + * Version: 1.0.9 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From d4a206ab1f715b29188c25dca5fedf2eaee52f43 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:47:20 +0800 Subject: [PATCH 40/49] donate link --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index 3c3bfe9..b2fbde4 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,6 @@ === UpdatePulse Server === Contributors: frogerme +Donate link: https://paypal.me/frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 From ec8856175ba9ae6377eafad6b30055f4f70a189f Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:18:53 +0800 Subject: [PATCH 41/49] Introduce constant `PUC_FORCE_BRANCH` to bypass tags & releases in VCS detection strategies --- lib/package-update-checker/Vcs/BitbucketApi.php | 5 ++++- lib/package-update-checker/Vcs/GitHubApi.php | 5 ++++- lib/package-update-checker/Vcs/GitLabApi.php | 5 ++++- readme.txt | 5 ++++- updatepulse-server.php | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/package-update-checker/Vcs/BitbucketApi.php b/lib/package-update-checker/Vcs/BitbucketApi.php index ec310d1..2fd864b 100644 --- a/lib/package-update-checker/Vcs/BitbucketApi.php +++ b/lib/package-update-checker/Vcs/BitbucketApi.php @@ -84,7 +84,10 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : return $this->get_branch( $config_branch ); }; - if ( ( 'main' === $config_branch || 'master' === $config_branch ) ) { + if ( + ( 'main' === $config_branch || 'master' === $config_branch ) && + ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ) { $strategies[ self::STRATEGY_LATEST_TAG ] = array( $this, 'get_latest_tag' ); } diff --git a/lib/package-update-checker/Vcs/GitHubApi.php b/lib/package-update-checker/Vcs/GitHubApi.php index 0526bb3..b39e35b 100644 --- a/lib/package-update-checker/Vcs/GitHubApi.php +++ b/lib/package-update-checker/Vcs/GitHubApi.php @@ -458,7 +458,10 @@ if ( ! class_exists( GitHubApi::class, false ) ) : protected function get_update_detection_strategies( $config_branch ) { $strategies = array(); - if ( 'main' === $config_branch || 'master' === $config_branch ) { + if ( + ( 'main' === $config_branch || 'master' === $config_branch ) && + ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ) { // Use the latest release. $strategies[ self::STRATEGY_LATEST_RELEASE ] = array( $this, 'get_latest_release' ); // Failing that, use the tag with the highest version number. diff --git a/lib/package-update-checker/Vcs/GitLabApi.php b/lib/package-update-checker/Vcs/GitLabApi.php index df3dced..45cba1d 100644 --- a/lib/package-update-checker/Vcs/GitLabApi.php +++ b/lib/package-update-checker/Vcs/GitLabApi.php @@ -440,7 +440,10 @@ if ( ! class_exists( GitLabApi::class, false ) ) : protected function get_update_detection_strategies( $config_branch ) { $strategies = array(); - if ( ( 'main' === $config_branch ) || ( 'master' === $config_branch ) ) { + if ( + ( 'main' === $config_branch ) || ( 'master' === $config_branch ) && + ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ) { $strategies[ self::STRATEGY_LATEST_RELEASE ] = array( $this, 'get_latest_release' ); $strategies[ self::STRATEGY_LATEST_TAG ] = array( $this, 'get_latest_tag' ); } diff --git a/readme.txt b/readme.txt index b2fbde4..e4e26f7 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://paypal.me/frogerme Tags: Plugin updates, Theme updates, WordPress updates, License Requires at least: 6.7 Tested up to: 6.7 -Stable tag: 1.0.9 +Stable tag: 1.0.10 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -129,6 +129,9 @@ This section describes how to install the plugin and get it working. == Changelog == += 1.0.10 = +* Introduce constant `PUC_FORCE_BRANCH` to bypass tags & releases in VCS detection strategies + = 1.0.9 = * Schedule mode: remove package metadata files when deleting packages * Schedule mode: make sure to reinitialise the update checker to avoid slug conflicts diff --git a/updatepulse-server.php b/updatepulse-server.php index 71c7a02..7a75ab4 100644 --- a/updatepulse-server.php +++ b/updatepulse-server.php @@ -3,7 +3,7 @@ * Plugin Name: UpdatePulse Server * Plugin URI: https://github.com/anyape/updatepulse-server/ * Description: Run your own update server. - * Version: 1.0.9 + * Version: 1.0.10 * Author: Alexandre Froger * Author URI: https://froger.me/ * License: GPLv2 or later From c6155be1facfd4674b98bc070fcb82d71e3934b3 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:33:02 +0800 Subject: [PATCH 42/49] Introduce constant `PUC_FORCE_BRANCH` to bypass tags & releases in VCS detection strategies --- README.md | 2 +- .../admin/plugin-remote-sources-page.php | 22 +++++++++++++++++-- .../Vcs/BitbucketApi.php | 2 +- lib/package-update-checker/Vcs/GitHubApi.php | 2 +- lib/package-update-checker/Vcs/GitLabApi.php | 2 +- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 62ae54a..ace678e 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Name | Type | Description Enable VCS | checkbox | Enables this server to download packages from a Version Control System before delivering updates.
Supports Bitbucket, Github and Gitlab.
If left unchecked, zip packages need to be manually uploaded to `wp-content/plugins/updatepulse-server/packages`. VCS URL | text | The URL of the Version Control System where packages are hosted.
Must follow the following pattern: `https://version-control-system.tld/username` where `https://version-control-system.tld` may be a self-hosted instance of Gitlab.
Each package repository URL must follow the following pattern: `https://version-control-system.tld/username/package-slug/`; the package files must be located at the root of the repository, and in the case of WordPress plugins the main plugin file must follow the pattern `package-slug.php`. Self-hosted VCS | checkbox | Check this only if the Version Control System is a self-hosted instance of Gitlab. -Packages branch name | text | The branch to download when getting remote packages from the Version Control System. +Packages branch name | text | The branch to download when getting remote packages from the Version Control System.
If the VCS supports releases or tags, they will be prioritised over the branch name (release first, then tag, then branch).
To bypass this behaviour, set the `PUC_FORCE_BRANCH` constant to `true` in `wp-config.php`. VCS credentials | text | Credentials for non-publicly accessible repositories.
In the case of Github and Gitlab, a Personal Access Token; in the case of Bitckucket, an App Password.
**WARNING: Keep these credentials secret, do not share them, and take care of renewing them before they expire!** Use Webhooks | checkbox | Check so that each repository of the Version Control System calls a Webhook when updates are pushed.
When checked, UpdatePulse Server will not regularly poll repositories for package version changes, but relies on events sent by the repositories to schedule a package download.
Webhook URL: `https://domain.tld/updatepulse-server-webhook/package-type/package-slug` - where `package-type` is the package type (`plugin`, `theme`, or `generic`) and `package-slug` is the slug of the package that needs updates.
Note that UpdatePulse Server does not rely on the content of the payload to schedule a package download, so any type of event can be used to trigger the Webhook. Remote Download Delay | number | Delay in minutes after which UpdatePulse Server will poll the Version Control System for package updates when the Webhook has been called.
Leave at `0` to schedule a package update during the cron run happening immediately after the Webhook notification was received. diff --git a/inc/templates/admin/plugin-remote-sources-page.php b/inc/templates/admin/plugin-remote-sources-page.php index 243cfd4..6fca6e5 100644 --- a/inc/templates/admin/plugin-remote-sources-page.php +++ b/inc/templates/admin/plugin-remote-sources-page.php @@ -107,7 +107,16 @@

- + PUC_FORCE_BRANCH, %3$s is true, %4$s is wp-config.php + esc_html__( 'The branch to download when getting remote packages from the Version Control System.%1$sIf the VCS supports releases or tags, they will be prioritised over the branch name (release first, then tag, then branch).%1$sTo bypass this behaviour and exclusively rely on the branch, set the %2$s constant to %3$s in %4$s.', 'updatepulse-server' ), + '
', + 'PUC_FORCE_BRANCH', + 'true', + 'wp-config.php' + ); + ?>

@@ -295,7 +304,16 @@

- + PUC_FORCE_BRANCH, %3$s is true, %4$s is wp-config.php + esc_html__( 'The branch to download when getting remote packages from the Version Control System.%1$sIf the VCS supports releases or tags, they will be prioritised over the branch name (release first, then tag, then branch).%1$sTo bypass this behaviour and exclusively rely on the branch, set the %2$s constant to %3$s in %4$s.', 'updatepulse-server' ), + '
', + 'PUC_FORCE_BRANCH', + 'true', + 'wp-config.php' + ); + ?>

diff --git a/lib/package-update-checker/Vcs/BitbucketApi.php b/lib/package-update-checker/Vcs/BitbucketApi.php index 2fd864b..754b2d3 100644 --- a/lib/package-update-checker/Vcs/BitbucketApi.php +++ b/lib/package-update-checker/Vcs/BitbucketApi.php @@ -86,7 +86,7 @@ if ( ! class_exists( BitbucketApi::class, false ) ) : if ( ( 'main' === $config_branch || 'master' === $config_branch ) && - ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ( ! defined( 'PUC_FORCE_BRANCH' ) || ! (bool) ( constant( 'PUC_FORCE_BRANCH' ) ) ) ) { $strategies[ self::STRATEGY_LATEST_TAG ] = array( $this, 'get_latest_tag' ); } diff --git a/lib/package-update-checker/Vcs/GitHubApi.php b/lib/package-update-checker/Vcs/GitHubApi.php index b39e35b..2bcddb4 100644 --- a/lib/package-update-checker/Vcs/GitHubApi.php +++ b/lib/package-update-checker/Vcs/GitHubApi.php @@ -460,7 +460,7 @@ if ( ! class_exists( GitHubApi::class, false ) ) : if ( ( 'main' === $config_branch || 'master' === $config_branch ) && - ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ( ! defined( 'PUC_FORCE_BRANCH' ) || ! (bool) ( constant( 'PUC_FORCE_BRANCH' ) ) ) ) { // Use the latest release. $strategies[ self::STRATEGY_LATEST_RELEASE ] = array( $this, 'get_latest_release' ); diff --git a/lib/package-update-checker/Vcs/GitLabApi.php b/lib/package-update-checker/Vcs/GitLabApi.php index 45cba1d..886d06c 100644 --- a/lib/package-update-checker/Vcs/GitLabApi.php +++ b/lib/package-update-checker/Vcs/GitLabApi.php @@ -442,7 +442,7 @@ if ( ! class_exists( GitLabApi::class, false ) ) : if ( ( 'main' === $config_branch ) || ( 'master' === $config_branch ) && - ( ! defined( PUC_FORCE_BRANCH ) || ! PUC_FORCE_BRANCH ) + ( ! defined( 'PUC_FORCE_BRANCH' ) || ! (bool) ( constant( 'PUC_FORCE_BRANCH' ) ) ) ) { $strategies[ self::STRATEGY_LATEST_RELEASE ] = array( $this, 'get_latest_release' ); $strategies[ self::STRATEGY_LATEST_TAG ] = array( $this, 'get_latest_tag' ); From 15ce0595123210b504338f018b0b1e253051d44a Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:35:17 +0800 Subject: [PATCH 43/49] email fix --- inc/templates/admin/plugin-help-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/templates/admin/plugin-help-page.php b/inc/templates/admin/plugin-help-page.php index c782712..001b6dd 100644 --- a/inc/templates/admin/plugin-help-page.php +++ b/inc/templates/admin/plugin-help-page.php @@ -285,7 +285,7 @@ Licensed With: another-plugin-or-theme-slug
// translators: %1$s is a link to opening an issue, %2$s is a contact email esc_html__( 'After reading the documentation, for more help on how to use UpdatePulse Server, please %1$s - bugfixes are welcome via pull requests, detailed bug reports with accurate pointers as to where and how they occur in the code will be addressed in a timely manner, and a fee will apply for any other request (if they are addressed). If and only if you found a security issue, please contact %2$s with full details for responsible disclosure.', 'updatepulse-server' ), '' . esc_html__( 'open an issue on Github', 'updatepulse-server' ) . '', - 'updatepulse@anyape.com', + 'updatepulse@anyape.com', ); ?>

From deda3090c312127531db33b531777c639bdec5e8 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:36:13 +0800 Subject: [PATCH 44/49] readme Companion Plugins --- readme.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/readme.txt b/readme.txt index e4e26f7..992c08a 100644 --- a/readme.txt +++ b/readme.txt @@ -47,6 +47,15 @@ This plugin adds the following major features to WordPress: * **API:** UpdatePulse Server provides APIs to manage packages and licenses. The APIs keys are secured with a system of tokens: the API keys are never shared over the network, acquiring a token requires signed payloads, and the tokens have a limited lifetime. For more details about tokens and security, see [the Nonce API documentation](https://github.com/anyape/updatepulse-server/blob/main/docs/misc.md#nonce-api). To connect their plugins or themes and UpdatePulse Server, developers can find integration examples in the [UpdatePulse Server Integration Examples](https://github.com/Anyape/updatepulse-server-integration) repository - theme and plugin examples rely heavily on the popular [Plugin Update Checker](https://github.com/YahnisElsts/plugin-update-checker) by [Yahnis Elsts](https://github.com/YahnisElsts). +== Companion Plugins == + +The following plugins are compatible with UpdatePulse Server and can be used to extend its functionality: +* [Updatepulse Blocks](https://store.anyape.com/product/updatepulse-blocks/): a seamless way to display packages from UpdatePulse Server directly within your site using the WordPress Block Editor or shortcodes. +* [UpdatePulse for WooCommerce](https://store.anyape.com/product/updatepulse-for-woocommerce/): a WooCommerce connector for UpdatePulse Server, allowing you to sell licensed packages through your WooCommerce store, either on the same WordPress installation or a separate store site. + +Developers are encouraged to build plugins and themes [integrated](https://github.com/anyape/updatepulse-server/blob/main/README.md) with UpdatePulse Server, leveraging its publicly available functions, actions and filters, or by making use of the provided APIs. + +If you wish to see your plugin added to this list, please [contact the author](mailto:updatepulse@anyape.com). == Troubleshooting == From e1fc90780e606ad1f186dc87021254f49c801bd7 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:37:47 +0800 Subject: [PATCH 45/49] typo fix --- inc/templates/admin/plugin-help-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/templates/admin/plugin-help-page.php b/inc/templates/admin/plugin-help-page.php index 001b6dd..197876a 100644 --- a/inc/templates/admin/plugin-help-page.php +++ b/inc/templates/admin/plugin-help-page.php @@ -52,7 +52,7 @@ printf( // translators: %s is upserv_download_remote_package( string $package_slug, string $type ); esc_html__( '[expert] calling the %s method in your own code, with the VCS-related parameters corresponding to a VCS configuration saved in UpdatePulse Server', 'updatepulse-server' ), - 'upserv_download_remote_package( string $package_slug, string $type, string $vcs_url = false, string branch = \'main\');' + 'upserv_download_remote_package( string $package_slug, string $type, string $vcs_url = false, string branch = \'main\' );' ); ?> From 575c7c3cdb21844a048028f9c09b7ba2400cd871 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:38:14 +0800 Subject: [PATCH 46/49] readme update --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index 992c08a..85edc62 100644 --- a/readme.txt +++ b/readme.txt @@ -140,6 +140,7 @@ This section describes how to install the plugin and get it working. = 1.0.10 = * Introduce constant `PUC_FORCE_BRANCH` to bypass tags & releases in VCS detection strategies +* Minor fix = 1.0.9 = * Schedule mode: remove package metadata files when deleting packages From 8efebb1774c0ed5c15a1caf8e3e38fa3b5601f2a Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:41:48 +0800 Subject: [PATCH 47/49] wl to readme links --- readme.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.txt b/readme.txt index 85edc62..10e48fa 100644 --- a/readme.txt +++ b/readme.txt @@ -50,8 +50,8 @@ To connect their plugins or themes and UpdatePulse Server, developers can find i == Companion Plugins == The following plugins are compatible with UpdatePulse Server and can be used to extend its functionality: -* [Updatepulse Blocks](https://store.anyape.com/product/updatepulse-blocks/): a seamless way to display packages from UpdatePulse Server directly within your site using the WordPress Block Editor or shortcodes. -* [UpdatePulse for WooCommerce](https://store.anyape.com/product/updatepulse-for-woocommerce/): a WooCommerce connector for UpdatePulse Server, allowing you to sell licensed packages through your WooCommerce store, either on the same WordPress installation or a separate store site. +* [Updatepulse Blocks](https://store.anyape.com/product/updatepulse-blocks/?wl=1): a seamless way to display packages from UpdatePulse Server directly within your site using the WordPress Block Editor or shortcodes. +* [UpdatePulse for WooCommerce](https://store.anyape.com/product/updatepulse-for-woocommerce/?wl=1): a WooCommerce connector for UpdatePulse Server, allowing you to sell licensed packages through your WooCommerce store, either on the same WordPress installation or a separate store site. Developers are encouraged to build plugins and themes [integrated](https://github.com/anyape/updatepulse-server/blob/main/README.md) with UpdatePulse Server, leveraging its publicly available functions, actions and filters, or by making use of the provided APIs. From 8356ac6ca27dd109adab5abe7bab1ae696051a69 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:40:26 +0800 Subject: [PATCH 48/49] fix nonce cleanup --- inc/nonce/class-nonce.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/inc/nonce/class-nonce.php b/inc/nonce/class-nonce.php index 2bce8a5..734ac66 100644 --- a/inc/nonce/class-nonce.php +++ b/inc/nonce/class-nonce.php @@ -588,15 +588,17 @@ class Nonce { $sql = "DELETE FROM {$wpdb->prefix}upserv_nonce WHERE expiry < %d AND ( - JSON_VALID(`data`) = 1 - AND ( - JSON_EXTRACT(`data` , '$.permanent') IS NULL - OR JSON_EXTRACT(`data` , '$.permanent') = 0 - OR JSON_EXTRACT(`data` , '$.permanent') = '0' - OR JSON_EXTRACT(`data` , '$.permanent') = false + JSON_VALID(`data`) = 0 + OR ( + JSON_VALID(`data`) = 1 + AND ( + JSON_EXTRACT(`data` , '$.permanent') IS NULL + OR JSON_EXTRACT(`data` , '$.permanent') = 0 + OR JSON_EXTRACT(`data` , '$.permanent') = '0' + OR JSON_EXTRACT(`data` , '$.permanent') = false + ) ) - ) OR - JSON_VALID(`data`) = 0;"; + );"; $sql_args = array( time() - self::DEFAULT_EXPIRY_LENGTH ); /** From 84da98bc6a86fb865ec4a6b0f4d2f3d43bf3bd24 Mon Sep 17 00:00:00 2001 From: Alexandre Froger <3622456+froger-me@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:38:22 +0800 Subject: [PATCH 49/49] Fix activation issue - `WP_Filesystem` call --- inc/manager/class-data-manager.php | 2 ++ readme.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/inc/manager/class-data-manager.php b/inc/manager/class-data-manager.php index d4454c3..01037c0 100644 --- a/inc/manager/class-data-manager.php +++ b/inc/manager/class-data-manager.php @@ -172,6 +172,8 @@ class Data_Manager { * @since 1.0.0 */ public static function maybe_setup_mu_plugin() { + WP_Filesystem(); + global $wp_filesystem; $result = true; diff --git a/readme.txt b/readme.txt index 10e48fa..fc0fc1d 100644 --- a/readme.txt +++ b/readme.txt @@ -141,6 +141,7 @@ This section describes how to install the plugin and get it working. = 1.0.10 = * Introduce constant `PUC_FORCE_BRANCH` to bypass tags & releases in VCS detection strategies * Minor fix +* Fix activation issue - `WP_Filesystem` call = 1.0.9 = * Schedule mode: remove package metadata files when deleting packages