Compare commits

...

15 commits
1.0.6 ... main

Author SHA1 Message Date
Alexandre Froger
4847ed1ed7 Merge remote-tracking branch 'origin/dev' into dev 2025-06-06 10:38:37 +08:00
Alexandre Froger
84da98bc6a Fix activation issue - WP_Filesystem call 2025-06-06 10:38:22 +08:00
Alexandre Froger
8356ac6ca2 fix nonce cleanup 2025-06-04 21:40:26 +08:00
Alexandre Froger
8efebb1774 wl to readme links 2025-04-28 10:41:48 +08:00
Alexandre Froger
575c7c3cdb readme update 2025-04-28 10:38:14 +08:00
Alexandre Froger
e1fc90780e typo fix 2025-04-28 10:37:47 +08:00
Alexandre Froger
deda3090c3 readme Companion Plugins 2025-04-28 10:36:13 +08:00
Alexandre Froger
15ce059512 email fix 2025-04-28 10:35:17 +08:00
Alexandre Froger
c6155be1fa Introduce constant PUC_FORCE_BRANCH to bypass tags & releases in VCS detection strategies 2025-04-22 11:33:02 +08:00
Alexandre Froger
ec8856175b Introduce constant PUC_FORCE_BRANCH to bypass tags & releases in VCS detection strategies 2025-04-22 10:18:53 +08:00
Alexandre Froger
d4a206ab1f donate link 2025-04-14 07:47:20 +08:00
Alexandre Froger
49adf7f6b4 remove package metadata files when deleting packages ; make sure to reinitialise the update checker to avoid slug conflicts 2025-04-13 13:44:15 +08:00
Alexandre Froger
2589b5071b Fix VCS candidates with webhook mode 2025-04-10 17:04:36 +08:00
Alexandre Froger
4bb83b4837 Fix scheduled mode package overrides 2025-04-10 13:14:29 +08:00
Alexandre Froger
0c3c1ab306 Full documentation MVP 2025-03-20 22:45:55 +08:00
19 changed files with 1073 additions and 140 deletions

View file

@ -151,7 +151,7 @@ Name | Type | Description
Enable VCS | checkbox | Enables this server to download packages from a Version Control System before delivering updates.<br/>Supports Bitbucket, Github and Gitlab.<br/>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.<br/>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.<br/>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.<br/>If the VCS supports releases or tags, they will be prioritised over the branch name (release first, then tag, then branch).<br/>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.<br/>In the case of Github and Gitlab, a Personal Access Token; in the case of Bitckucket, an App Password.<br/>**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.<br>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.<br>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.<br>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.<br>Leave at `0` to schedule a package update during the cron run happening immediately after the Webhook notification was received.

View file

@ -424,7 +424,6 @@ class Webhook_API {

if ( 0 === strpos( $config['url'], trailingslashit( $url ) ) ) {
$vcs_candidates[] = $config;
$vcs_candidates[] = $config;
}
}
}

View file

@ -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(
'<a href="' . admin_url( 'admin.php?page=upserv-page-help' ) . '">' . __( 'Help', 'updatepulse-server' ) . '</a>',
@ -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( '<div class="%1$s"><p>%2$s</p></div>', $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( '<div class="%1$s"><p>%2$s</p></div>', $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 '<h1>' . esc_html__( 'UpdatePulse Server', 'updatepulse-server' ) . '</h1>';

@ -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 ) ) {


View file

@ -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':

View file

@ -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;

View file

@ -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;


View file

@ -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 );

View file

@ -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 );

/**

View file

@ -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;
}


View file

@ -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'] ) ) {

View file

@ -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' => '<input type="checkbox" />',
@ -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' );


View file

@ -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'] ) {

View file

@ -52,7 +52,7 @@
printf(
// translators: %s is <code>upserv_download_remote_package( string $package_slug, string $type );</code>
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' ),
'<code>upserv_download_remote_package( string $package_slug, string $type, string $vcs_url = false, string branch = \'main\');</code>'
'<code>upserv_download_remote_package( string $package_slug, string $type, string $vcs_url = false, string branch = \'main\' );</code>'
);
?>
</li>
@ -285,7 +285,7 @@ Licensed With: another-plugin-or-theme-slug</pre><br>
// 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' ),
'<a target="_blank" href="https://github.com/anyape/updatepulse-server/issues">' . esc_html__( 'open an issue on Github', 'updatepulse-server' ) . '</a>',
'<a href="mailto:updatepulse@anyape.come">updatepulse@anyape.com</a>',
'<a href="mailto:updatepulse@anyape.com">updatepulse@anyape.com</a>',
);
?>
</p>

View file

@ -107,7 +107,16 @@
<td>
<input class="vcs-setting regular-text" type="text" id="upserv_vcs_branch" data-prop="branch" value="">
<p class="description">
<?php esc_html_e( 'The branch to download when getting remote packages from the Version Control System.', 'updatepulse-server' ); ?>
<?php
printf(
// translators: %1$s line break, %2$s is <code>PUC_FORCE_BRANCH</code>, %3$s is <code>true</code>, %4$s is <code>wp-config.php</code>
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' ),
'<br/>',
'<code>PUC_FORCE_BRANCH</code>',
'<code>true</code>',
'<code>wp-config.php</code>'
);
?>
</p>
</td>
</tr>
@ -295,7 +304,16 @@
<td>
<input class="regular-text" type="text" id="upserv_add_vcs_branch" data-prop="branch" value="">
<p class="description">
<?php esc_html_e( 'The branch to download when getting remote packages from the Version Control System.', 'updatepulse-server' ); ?>
<?php
printf(
// translators: %1$s line break, %2$s is <code>PUC_FORCE_BRANCH</code>, %3$s is <code>true</code>, %4$s is <code>wp-config.php</code>
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' ),
'<br/>',
'<code>PUC_FORCE_BRANCH</code>',
'<code>true</code>',
'<code>wp-config.php</code>'
);
?>
</p>
</td>
</tr>

View file

@ -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' );
}


View file

@ -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.

View file

@ -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' );
}

View file

@ -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)

View file

@ -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