Files
Malin 3248cbb029 feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.

Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
  - Provider key: s3compatible
  - Reads user-configured endpoint URL from settings
  - Uses path-style URL access (required by most S3-compatible services)
  - Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
    AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
  - Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
2026-03-03 12:30:18 +01:00

700 lines
21 KiB
PHP

<?php
/**
* Updates Class
*
* @package deliciousbrains
* @subpackage api/updates
* @copyright Copyright (c) 2015, Delicious Brains
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 0.1
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Delicious_Brains_API_Updates Class
*
* This class handles the updates to a plugin
*
* @since 0.1
*/
class Delicious_Brains_API_Updates {
/**
* @var Delicious_Brains_API_Licences
*/
protected $licences;
/**
* @var array
*/
private $plugin_notices = array();
/**
* Initiate update class
*
* @param Delicious_Brains_API_Licences $licences
*/
function __construct( Delicious_Brains_API_Licences $licences ) {
$this->licences = $licences;
if ( ( is_admin() ) || is_network_admin() || ( defined( 'WP_CLI' ) && class_exists( 'WP_CLI' ) ) ) {
add_filter( 'plugins_api', array( $this, 'short_circuit_wordpress_org_plugin_info_request' ), 10, 3 );
add_filter( 'http_response', array( $this, 'verify_download' ), 10, 3 );
add_filter( 'plugins_api', array( $this, 'inject_addon_install_resource' ), 10, 3 );
add_action( 'current_screen', array( $this, 'check_again_clear_transients' ) );
}
$this->add_plugin_notice( $this->licences->plugin->basename );
if ( $this->licences->addons ) {
foreach ( $this->licences->addons as $basename => $addon ) {
if ( ! ( $addon['available'] && $addon['installed'] ) ) {
// Only register addons for updates that are installed and available for license
continue;
}
$this->add_plugin_notice( $basename );
}
}
}
/**
* Add handlers for displaying update notices for a plugin on our settings page.
*
* @param string $basename
*/
public function add_plugin_notice( string $basename ) {
if ( empty( $basename ) ) {
return;
}
$this->plugin_notices[ $basename ] = $basename;
}
/**
* Return update notice info for a plugin on our settings page.
*
* @return array
*/
public function get_plugin_update_notices(): array {
$notices = array();
if ( ! empty( $this->plugin_notices ) ) {
foreach ( $this->plugin_notices as $basename ) {
$notice = $this->version_update_notice( $basename );
if ( ! empty( $notice ) ) {
$notices[] = $notice;
}
}
}
return $notices;
}
/**
* Adds the plugin to the array of plugins used by the update JS
*
* @param array $plugins
*
* @return array
*/
public function register_plugin_for_updates( $plugins ) {
$plugins[ $this->licences->plugin->slug ] = array_merge(
(array) $this->licences->plugin,
array(
'addons' => $this->licences->addons,
'license' => $this->licences->is_licence_expired(),
'update_available' => $this->update_available( $this->licences->plugin->slug ),
)
);
return $plugins;
}
/**
* Take over the update check for plugins
*
* @param object $trans
*
* @return object
*/
function site_transient_update_plugins( $trans ) {
if ( ! is_object( $trans ) ) {
return $trans;
}
if ( ! isset( $trans->response ) || ! is_array( $trans->response ) ) {
$trans->response = array();
}
if ( ! isset( $trans->no_update ) || ! is_array( $trans->no_update ) ) {
$trans->no_update = array();
}
$plugin_upgrade_data = $this->get_upgrade_data();
$plugin_basename = $this->licences->plugin->get_plugin_basename( $this->licences->plugin->slug );
if ( isset( $trans->no_update[ $plugin_basename ] ) ) {
// Ensure the pro plugin always has the correct info and WP API doesn't confuse with free version
$trans->no_update[ $plugin_basename ]->slug = $this->licences->plugin->slug;
$trans->no_update[ $plugin_basename ]->url = $this->licences->api_base;
$trans->no_update[ $plugin_basename ]->new_version = $this->licences->plugin->version;
}
if ( false === $plugin_upgrade_data || ! isset( $plugin_upgrade_data[ $this->licences->plugin->slug ] ) ) {
return $trans;
}
foreach ( $plugin_upgrade_data as $plugin_slug => $upgrade_data ) {
$plugin_basename = $this->licences->plugin->get_plugin_basename( $plugin_slug );
if ( isset( $this->licences->addons[ $plugin_basename ] ) && ! ( $this->licences->addons[ $plugin_basename ]['available'] && $this->licences->addons[ $plugin_basename ]['installed'] ) ) {
// Addon not installed or available for license, ignore
continue;
}
$installed_version = $this->get_installed_version( $plugin_slug );
$latest_version = $this->get_latest_version( $plugin_slug, $installed_version );
if ( false === $installed_version || false === $latest_version ) {
continue;
}
if ( version_compare( $installed_version, $latest_version, '<' ) ) {
$is_beta = $this->is_beta_version( $latest_version );
$trans->response[ $plugin_basename ] = new stdClass();
$trans->response[ $plugin_basename ]->url = $this->licences->api_base;
$trans->response[ $plugin_basename ]->slug = $plugin_slug;
$trans->response[ $plugin_basename ]->package = $this->get_plugin_update_download_url( $plugin_slug, $is_beta );
$trans->response[ $plugin_basename ]->new_version = $latest_version;
$trans->response[ $plugin_basename ]->id = '0';
$trans->response[ $plugin_basename ]->plugin = $plugin_basename;
if ( isset( $upgrade_data['requires_php'] ) ) {
$trans->response[ $plugin_basename ]->requires_php = $upgrade_data['requires_php'];
}
if ( isset( $upgrade_data['tested'] ) ) {
$trans->response[ $plugin_basename ]->tested = $upgrade_data['tested'];
}
if ( isset( $upgrade_data['icon_url'] ) ) {
$trans->response[ $plugin_basename ]->icons['svg'] = $upgrade_data['icon_url'];
}
}
}
return $trans;
}
/**
* Add some custom JS into the plugin page for our updates process
*/
public function enqueue_plugin_update_script() {
$handle = 'as3cf-dbrains-plugin-update-script';
// This script should only be enqueued once if the site has multiple
// Delicious Brains plugins installed
if ( wp_script_is( $handle, 'enqueued' ) ) {
return;
}
$version = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? time() : $this->licences->plugin->version;
$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
$src = plugins_url( "assets/js/plugin-update$min.js", __FILE__ );
wp_enqueue_script( $handle, $src, array( 'jquery', 'underscore' ), $version, true );
wp_localize_script( $handle,
'as3cf_dbrains',
array(
'nonces' => array(
'check_licence' => wp_create_nonce( 'check-licence' ),
),
'strings' => array(
'check_licence_again' => __( 'Check My License Again' ),
'licence_check_problem' => __( 'A problem occurred when trying to check the license, please try again.' ),
'requires_parent_licence' => __( 'Requires a valid license for %s.' ),
'update_available' => __( 'New version available for active subscriptions.' ),
),
'plugins' => apply_filters( 'as3cf_plugins', array() ),
)
);
$src = plugins_url( "assets/css/plugin-update.css", __FILE__ );
wp_enqueue_style( $handle, $src, array(), $version );
}
/**
* Short circuits the HTTP request to WordPress.org servers to retrieve plugin information.
* Will only fire on the update-core.php admin page.
*
* @param object|bool $res Plugin resource object or boolean false.
* @param string $action The API call being performed.
* @param object $args Arguments for the API call being performed.
*
* @return object|bool Plugin resource object or boolean false.
*/
function short_circuit_wordpress_org_plugin_info_request( $res, $action, $args ) {
if ( 'plugin_information' != $action || empty( $args->slug ) || $this->licences->plugin->slug != $args->slug ) {
return $res;
}
$screen = get_current_screen();
// Only fire on the update-core.php admin page
if ( empty( $screen->id ) || ( 'update-core' !== $screen->id && 'update-core-network' !== $screen->id ) ) {
return $res;
}
$res = new stdClass();
$plugin_info = $this->get_upgrade_data();
if ( isset( $plugin_info[ $this->licences->plugin->slug ]['tested'] ) ) {
$res->tested = $plugin_info[ $this->licences->plugin->slug ]['tested'];
} else {
$res->tested = false;
}
return $res;
}
/**
* Clear update transients when the user clicks the "Check Again" button from the update screen.
*
* @param object $current_screen
*/
function check_again_clear_transients( $current_screen ) {
if ( ! isset( $current_screen->id ) || false === strpos( $current_screen->id, 'update-core' ) || ! isset( $_GET['force-check'] ) ) {
return;
}
delete_site_transient( $this->licences->plugin->prefix . '_upgrade_data' );
delete_site_transient( 'update_plugins' );
delete_site_transient( $this->licences->plugin->prefix . '_licence_response' );
delete_site_transient( 'dbrains_api_down' );
}
/**
* Display our custom plugin details when the user clicks "view details"
* on the plugin listing page.
*/
function plugin_update_popup() {
$slug = sanitize_key( $_GET['plugin'] );
if ( ! $this->is_plugin( $slug ) ) {
return;
}
$error_msg = '<p>' . __( 'Could not retrieve version details. Please try again.' ) . '</p>';
$latest_version = $this->get_latest_version( $slug );
if ( false === $latest_version ) {
echo $error_msg;
exit;
}
$data = $this->licences->get_changelog( $slug, $this->is_beta_version( $latest_version ) );
if ( is_wp_error( $data ) || empty( $data ) ) {
echo $error_msg;
} else {
echo $data;
}
exit;
}
/**
* Verify the download URL for the plugin
*
* @param array $response
* @param array $args
* @param string $url
*
* @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error
* instance upon error
*/
function verify_download( $response, $args, $url ) {
if ( is_wp_error( $response ) ) {
return $response;
}
$download_url = $this->get_plugin_update_download_url( $this->licences->plugin->slug );
if ( false === strpos( $url, $download_url ) || 402 != $response['response']['code'] ) {
return $response;
}
// The $response['body'] is blank but output is actually saved to a file in this case
$data = @file_get_contents( $response['filename'] ); //phpcs:ignore
if ( ! $data ) {
return new WP_Error( $this->licences->plugin->prefix . '_download_error_empty', sprintf( __( 'Error retrieving download from deliciousbrain.com. Please try again or download manually from <a href="%1$s">%2$s</a>.' ), $this->licences->plugin->account_url, _x( 'My Account', 'Delicious Brains account' ) ) );
}
$decoded_data = json_decode( $data, true );
// Can't decode the JSON errors, so just barf it all out
if ( ! isset( $decoded_data['errors'] ) || ! $decoded_data['errors'] ) {
return new WP_Error( $this->licences->plugin->prefix . '_download_error_raw', $data );
}
foreach ( $decoded_data['errors'] as $key => $msg ) {
return new WP_Error( $this->licences->plugin->prefix . '_' . $key, $msg );
}
}
/**
* Is the plugin version a beta version
*
* @param string $ver
*
* @return bool
*/
function is_beta_version( $ver ) {
if ( preg_match( '@b[0-9]+$@', $ver ) ) {
return true;
}
return false;
}
/**
* Get the installed version of a plugin
*
* @param string $slug
*
* @return int|string
*/
function get_installed_version( $slug ) {
$meta_key = $this->licences->plugin->get_global_meta_key();
if ( isset( $GLOBALS[ $meta_key ][ $slug ]['version'] ) ) {
// Plugin activated, use meta for version
$installed_version = $GLOBALS[ $meta_key ][ $slug ]['version'];
} else {
// Not activated
$installed_version = $this->licences->plugin->get_plugin_version( $slug );
}
return $installed_version;
}
/**
* Get the latest version available of a plugin
*
* @param string $slug
* @param string|null $installed_version
*
* @return string|false
*/
function get_latest_version( $slug, $installed_version = null ) {
$data = $this->get_upgrade_data();
if ( ! isset( $data[ $slug ] ) ) {
return false;
}
$plugin_file = $this->licences->plugin->get_plugin_basename( $slug );
if ( $this->is_addon( $plugin_file ) ) {
if ( ! $this->is_allowed_addon( $slug ) ) {
return false;
}
}
if ( is_null( $installed_version ) ) {
$installed_version = $this->get_installed_version( $slug );
}
$required_version = $this->get_required_version( $slug );
// Return the latest beta version if the installed version is beta
// and the API returned a beta version and it's newer than the latest stable version
if (
$installed_version
&& ( $this->is_beta_version( $installed_version ) || $this->is_beta_version( $required_version ) )
&& isset( $data[ $slug ]['beta_version'] )
&& version_compare( $data[ $slug ]['version'], $data[ $slug ]['beta_version'], '<' )
) {
return $data[ $slug ]['beta_version'];
}
return $data[ $slug ]['version'];
}
/**
* Get the upgrade data about the plugin and its addons
*
* @return array|bool|mixed
*/
function get_upgrade_data() {
$info = get_site_transient( $this->licences->plugin->prefix . '_upgrade_data' );
// Remove old format upgrade data.
if ( isset( $info['version'] ) ) {
delete_site_transient( $this->licences->plugin->prefix . '_licence_response' );
delete_site_transient( $this->licences->plugin->prefix . '_upgrade_data' );
$info = false;
}
if ( $info ) {
return $info;
}
$data = $this->licences->get_upgrade_data();
$data = json_decode( $data, true );
/*
We need to set the transient even when there's an error,
otherwise we'll end up making API requests over and over again
and slowing things down big time.
*/
$default_upgrade_data = array( $this->licences->plugin->slug => array( 'version' => $this->licences->plugin->version ) );
if ( ! $data ) {
set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $default_upgrade_data, $this->licences->transient_retry_timeout );
$this->licences->log_error( 'Error trying to decode JSON upgrade data.' );
return false;
}
if ( isset( $data['errors'] ) ) {
set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $default_upgrade_data, $this->licences->transient_retry_timeout );
$this->licences->log_error( __( 'Error trying to get upgrade data.' ), $data['errors'] );
return false;
}
set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $data, $this->licences->transient_timeout );
return $data;
}
/**
* Create the plugin update URL
*
* @param string $plugin_slug
* @param bool $is_beta
*
* @return string
*/
function get_plugin_update_download_url( $plugin_slug, $is_beta = false ) {
$licence = $this->licences->get_licence_key();
$query_args = array(
'request' => 'download',
'licence_key' => $licence,
'slug' => $plugin_slug,
'product' => $this->licences->plugin->slug,
'site_url' => $this->licences->home_url,
);
if ( $is_beta ) {
$query_args['beta'] = '1';
}
$url = add_query_arg( $query_args, $this->licences->api_url );
return esc_url_raw( $url );
}
/**
* Display custom version update notices to the top of our plugin page
*
* @param string $basename
*
* @return array
*/
private function version_update_notice( string $basename ): array {
// We don't want to show both the "Update Required" and "Update Available" messages at the same time
if ( $this->is_addon( $basename ) && $this->is_addon_outdated( $basename ) ) {
return array();
}
$slug = current( explode( '/', $basename ) );
// To reduce UI clutter we hide addon update notices if the core plugin has updates available
if ( $this->is_addon( $basename ) && $this->core_update_available() ) {
// Core update is available, don't show update notices for addons until core is updated
return array();
}
$installed_version = $this->get_installed_version( $slug );
$latest_version = $this->get_latest_version( $slug, $installed_version );
if ( version_compare( $installed_version, $latest_version, '<' ) ) {
$plugin_name = ( isset( $this->licences->addons[ $basename ] ) ) ? $this->licences->addons[ $basename ]['name'] : $this->licences->plugin->name;
$heading = _x( 'Update Available', 'A new version of the plugin is available' );
$message = sprintf( __( '%1$s %2$s is now available. You currently have %3$s installed.' ), $plugin_name, $latest_version, $installed_version );
$licence = $this->licences->get_licence_key();
$licence_response = $this->licences->is_licence_expired();
$licence_problem = isset( $licence_response['errors'] );
if ( ! empty( $licence ) && ! $licence_problem ) {
$update_url = wp_nonce_url( $this->licences->admin_url( 'update.php?action=upgrade-plugin&plugin=' . urlencode( $basename ) ), 'upgrade-plugin_' . $basename );
$message .= ' ' . sprintf( '<a href="%1$s">%2$s</a>', $update_url, _x( 'Update Now', 'Download and install a new version of the plugin' ) );
}
return array(
'slug' => $slug,
'heading' => $heading,
'message' => $message,
);
}
return array();
}
/**
* Is the plugin an addon
*
* @param string $plugin_file
*
* @return bool
*/
function is_addon( $plugin_file ) {
return isset( $this->licences->addons[ $plugin_file ] );
}
/**
* Is there an update available for the given plugin slug?
*
* @param string $slug
*
* @return bool
*/
private function update_available( string $slug ): bool {
$installed_version = $this->get_installed_version( $slug );
$latest_version = $this->get_latest_version( $slug, $installed_version );
return version_compare( $installed_version, $latest_version, '<' );
}
/**
* Is there an update for the core plugin?
*
* @return bool
*/
private function core_update_available(): bool {
return $this->update_available( $this->licences->plugin->slug );
}
/**
* Check if an addon needs to be updated
*
* @param string $addon_basename
*
* @return bool
*/
function is_addon_outdated( $addon_basename ) {
$addon_slug = current( explode( '/', $addon_basename ) );
$installed_version = $this->get_installed_version( $addon_slug );
$required_version = $this->licences->addons[ $addon_basename ]['required_version'];
return version_compare( $installed_version, $required_version, '<' );
}
/**
* Is the addon allowed for the license
*
* @param string $slug
*
* @return bool
*/
function is_allowed_addon( $slug ) {
$addons = get_site_transient( $this->licences->plugin->prefix . '_addons' );
if ( isset( $addons[ $slug ] ) ) {
// Addon allowed
return true;
}
// Not an allowed addon
return false;
}
/**
* Hook into the plugin install process and inject addon download url
*
* @param stdClass $res
* @param string $action
* @param stdClass $args
*
* @return stdClass
*/
function inject_addon_install_resource( $res, $action, $args ) {
if ( 'plugin_information' != $action || empty( $args->slug ) ) {
return $res;
}
$addons = get_site_transient( $this->licences->plugin->prefix . '_addons' );
if ( ! isset( $addons[ $args->slug ] ) ) {
return $res;
}
$addon = $addons[ $args->slug ];
$required_version = $this->get_required_version( $args->slug );
$is_beta = $this->is_beta_version( $required_version ) && ! empty( $addon['beta_version'] );
$res = new stdClass();
$res->name = $this->licences->plugin->name . ' ' . $addon['name'];
$res->version = $is_beta ? $addon['beta_version'] : $addon['version'];
$res->download_link = $this->get_plugin_update_download_url( $args->slug, $is_beta );
$res->tested = isset( $addon['tested'] ) ? $addon['tested'] : false;
return $res;
}
/**
* Get the required version of a plugin
*
* @param string $slug
*
* @return string
*/
function get_required_version( $slug ) {
$plugin_file = $this->licences->plugin->get_plugin_basename( $slug );
if ( isset( $this->licences->addons[ $plugin_file ]['required_version'] ) ) {
return $this->licences->addons[ $plugin_file ]['required_version'];
} else {
return '0';
}
}
/**
* Check a plugin slug to see if it is the core plugin or one of its addons
*
* @param string $slug
*
* @return bool
*/
function is_plugin( $slug ) {
$plugins = array( $this->licences->plugin->slug );
if ( $this->licences->addons ) {
foreach ( $this->licences->addons as $key => $addon ) {
$plugins[] = dirname( $key );
}
}
return in_array( $slug, $plugins );
}
/**
* Helper to get the directory of the plugin
*
* @return string
*/
function plugins_dir() {
$path = untrailingslashit( $this->licences->plugin->dir_path );
return substr( $path, 0, strrpos( $path, DIRECTORY_SEPARATOR ) ) . DIRECTORY_SEPARATOR;
}
}