Files
WPS3Media/classes/pro/tool.php
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

431 lines
9.6 KiB
PHP

<?php
namespace DeliciousBrains\WP_Offload_Media\Pro;
use Amazon_S3_And_CloudFront_Pro;
use AS3CF_Pro_Utils;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Items\Item;
abstract class Tool {
/**
* @var Amazon_S3_And_CloudFront_Pro
*/
protected $as3cf;
/**
* @var string
*/
protected $prefix = 'as3cf';
/**
* @var string
*/
protected $tab = 'tools';
/**
* @var string
*/
protected $type = 'tool';
/**
* @var string
*/
protected $view = 'tool';
/**
* @var int
*/
protected $priority = 0;
/**
* @var string
*/
protected $tool_key;
/**
* @var string
*/
protected $tool_slug;
/**
* @var string
*/
protected $errors_key_prefix;
/**
* @var string
*/
protected $errors_key;
/**
* @var array
*/
protected static $show_tool_constants = array();
/**
* @var bool
*/
protected static $requires_bucket_access = true;
/**
* AS3CF_Tool constructor.
*
* @param Amazon_S3_And_CloudFront_Pro $as3cf
*/
public function __construct( $as3cf ) {
$this->as3cf = $as3cf;
$this->tool_slug = str_replace( array( ' ', '_' ), '-', $this->tool_key );
$this->errors_key_prefix = 'as3cf_tool_errors_';
$this->errors_key = $this->errors_key_prefix . $this->tool_key;
}
/**
* Initialize the tool.
*/
public function init() {
add_filter( 'as3cfpro_js_strings', array( $this, 'add_js_strings' ) );
add_filter( 'as3cfpro_js_settings', array( $this, 'add_js_settings' ) );
// Notices.
add_filter( 'as3cf_get_notices', array( $this, 'maybe_add_tool_errors_to_notice' ), 10, 3 );
}
/**
* Add strings for the Tools to the Javascript
*
* @param array $strings
*
* @return array
*
* Note: To be overridden by a tool if required.
*/
public function add_js_strings( $strings ) {
return $strings;
}
/**
* Add settings for the Tools to the Javascript
*
* @param array $settings
*
* @return array
*/
public function add_js_settings( $settings ) {
return $settings;
}
/**
* Priority
*
* @param int $priority
*
* @return $this
*/
public function priority( $priority ) {
$this->priority = $priority;
return $this;
}
/**
* Get tools key.
*
* @return string
*/
public function get_tool_key() {
return $this->tool_key;
}
/**
* Get tab.
*
* @return string
*/
public function get_tab() {
return $this->tab;
}
/**
* Get info for tool, including current status.
*
* @return array
*/
public function get_info() {
return array(
'id' => $this->tool_key,
'tab' => $this->tab,
'priority' => $this->priority,
'slug' => $this->tool_slug,
'type' => $this->type,
'render' => $this->should_render(),
'is_processing' => $this->is_processing(),
'requires_bucket_access' => $this->requires_bucket_access(),
);
}
/**
* Should we render the tool's UI?
*
* @return bool
*/
public function should_render() {
return true;
}
/**
* Are we currently processing?
*
* @return bool
*/
protected function is_processing() {
return false;
}
/**
* Is queued?
*
* @return bool
*/
public function is_queued(): bool {
return false;
}
/**
* Is the tool currently active, e.g. starting, working, paused or finishing up?
*
* @return bool
*/
public function is_active(): bool {
return false;
}
/**
* Get the errors created by the tool
*
* @param array $default
*
* @return array
*/
public function get_errors( $default = array() ) {
return get_site_option( $this->errors_key, $default );
}
/**
* Update the saved errors for the tool
*
* @param array $errors
*/
public function update_errors( $errors ) {
update_site_option( $this->errors_key, $errors );
}
/**
* Clear all errors created by the tool
*/
protected function clear_errors() {
delete_site_option( $this->errors_key );
}
/**
* Update the error notice
*
* @param array $errors
*/
public function update_error_notice( $errors = array() ) {
if ( empty( $errors ) ) {
$errors = $this->get_errors();
}
if ( ! empty( $errors ) ) {
$args = array(
'type' => 'error',
'class' => 'tool-error',
'flash' => false,
'only_show_to_user' => false,
'only_show_on_tab' => $this->tab,
'custom_id' => $this->errors_key,
'user_capabilities' => array( 'as3cfpro', 'is_plugin_setup' ),
);
// Try and re-use some of existing notice to avoid churn in db or front end.
$existing_notice = $this->as3cf->notices->find_notice_by_id( $this->errors_key );
if ( ! empty( $existing_notice ) ) {
$args = array_merge( $existing_notice, $args );
}
$message = $this->get_error_notice_message();
$this->as3cf->notices->add_notice( $message, $args );
} else {
$this->as3cf->notices->remove_notice_by_id( $this->errors_key );
}
}
/**
* Undismiss error notice for all users.
*/
public function undismiss_error_notice() {
$this->as3cf->notices->undismiss_notice_for_all( $this->errors_key );
}
/**
* Dismiss one or all errors for a source item.
*
* @param int $blog_id
* @param string $source_type
* @param int $source_id
* @param string|int $errors Optional indicator of which error to dismiss for source item, default 'all'.
*/
public function dismiss_errors( $blog_id, $source_type, $source_id, $errors = 'all' ) {
$saved_errors = $this->get_errors();
foreach ( $saved_errors as $idx => &$saved_error ) {
if ( $saved_error->blog_id !== $blog_id ) {
continue;
}
if ( $saved_error->source_type !== $source_type || $saved_error->source_id != $source_id ) {
continue;
}
// Remove all errors for this source item?
if ( $errors === 'all' ) {
unset( $saved_errors[ $idx ] );
break;
}
// If the saved error message for this item is an array, remove just the one index
if ( isset( $saved_error->messages[ $errors ] ) ) {
// Break the object reference. See GitHub issue #2635
$saved_error = clone $saved_error;
unset( $saved_error->messages[ $errors ] );
// If the array is now empty, remove the entire error item
if ( empty( $saved_error->messages ) ) {
unset( $saved_errors[ $idx ] );
} else {
// Force a reindex of the array to avoid issues with JSON encoding switching to Object if there's non-sequential numeric keys.
$saved_error->messages = array_values( $saved_error->messages );
}
}
// Whether we dismissed anything or not, we found and processed the expected source item's errors.
break;
}
$updated = AS3CF_Pro_Utils::array_prune_recursive( $saved_errors );
$this->update_errors( $updated );
$this->update_error_notice();
}
/**
* Maybe add error details to this tool's error notice.
*
* @param array $notices An array of notices.
* @param string $tab Optionally restrict to notifications for a specific tab.
* @param bool $all_tabs Optionally return all tab specific notices regardless of tab.
*
* @return array
*/
public function maybe_add_tool_errors_to_notice( array $notices, $tab = '', $all_tabs = false ) {
if ( ! empty( $notices ) ) {
$errors = $this->get_errors();
if ( empty( $errors ) ) {
return $notices;
}
foreach ( $notices as $idx => $notice ) {
if (
! empty( $notice['class'] ) &&
'tool-error' === $notice['class'] &&
! empty( $notice['id'] ) &&
$notice['id'] === $this->errors_key
) {
$details = array();
foreach ( $errors as $error ) {
// If the error is stored as an array, it's almost certainly stored
// in a previous format/structure that we can't render properly.
// This will be corrected by the upgrade process, but that process may
// not have completed yet.
if ( is_array( $error ) ) {
continue;
}
/** @var Item $class */
$class = $this->as3cf->get_source_type_class( $error->source_type );
$this->as3cf->switch_to_blog( $error->blog_id );
$details[] = array(
'blog_id' => $error->blog_id,
'source_type' => $error->source_type,
'source_type_name' => $this->as3cf->get_source_type_name( $error->source_type ),
'source_id' => $error->source_id,
'edit_url' => $class::admin_link( $error ),
'messages' => $error->messages,
);
$this->as3cf->restore_current_blog();
}
$notices[ $idx ]['errors'] = array(
'tool_key' => $this->tool_key,
'details' => $details,
);
break;
}
}
}
return $notices;
}
/**
* Tool specific message for error notice.
*
* @param string|null $message Optional message to override the default for the tool.
*
* @return string
*/
protected function get_error_notice_message( $message = null ) {
return '';
}
/**
* Get the constant used to define whether tool should always be shown (implemented as required by subclass).
*
* @return string|false Constant name if defined, otherwise false
*/
public static function show_tool_constant() {
return AS3CF_Utils::get_first_defined_constant( static::$show_tool_constants );
}
/**
* Count media files in bucket.
*
* @return int
*/
protected function count_offloaded_media_files() {
static $count;
if ( is_null( $count ) ) {
$media_counts = $this->as3cf->media_counts();
$count = $media_counts['offloaded'];
}
return $count;
}
/**
* Does the tool need authenticated access to the bucket?
*
* @return bool
*/
public static function requires_bucket_access(): bool {
return static::$requires_bucket_access;
}
}