';
@@ -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/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/inc/manager/class-package-manager.php b/inc/manager/class-package-manager.php
index fdc28fe..5fc4d8c 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,20 @@ class Package_Manager {
do_action( 'upserv_package_manager_deleted_package', $slug, $result );
if ( $result ) {
+ $scheduled_hook = 'upserv_check_remote_' . $slug;
+
upserv_unwhitelist_package( $slug );
+ upserv_set_package_metadata( $slug, null );
+ 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/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 );
/**
diff --git a/inc/server/update/class-update-server.php b/inc/server/update/class-update-server.php
index af3cb4c..ace147e 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;
@@ -477,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(
@@ -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 );
}
@@ -1249,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/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/inc/templates/admin/plugin-help-page.php b/inc/templates/admin/plugin-help-page.php
index c782712..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\' );'
);
?>
@@ -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',
);
?>
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 ec310d1..754b2d3 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' ) || ! (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 0526bb3..2bcddb4 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' ) || ! (bool) ( constant( '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..886d06c 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' ) || ! (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' );
}
diff --git a/readme.txt b/readme.txt
index 4e90486..fc0fc1d 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,9 +1,10 @@
=== 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
-Stable tag: 1.0.6
+Stable tag: 1.0.10
Requires PHP: 8.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -46,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/?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.
+
+If you wish to see your plugin added to this list, please [contact the author](mailto:updatepulse@anyape.com).
== Troubleshooting ==
@@ -61,6 +71,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.9 =
+
+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 +138,22 @@ 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
+* Minor fix
+* Fix activation issue - `WP_Filesystem` call
+
+= 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
+
+= 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..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.6
+ * Version: 1.0.10
* Author: Alexandre Froger
* Author URI: https://froger.me/
* License: GPLv2 or later