From 0681f4d888c0c4c1a54b62f8695cda64dda3e170 Mon Sep 17 00:00:00 2001 From: Anton Tanchin Date: Wed, 21 Jan 2026 16:55:42 +0300 Subject: [PATCH] Add S3 storage type selection and MinIO path-style URL support to W3 Total Cache - Configuration key `cdn.s3_compatible.storage_type` in `ConfigKeys.php` - UI dropdown for storage type selection (Auto/AWS/MinIO) in CDN settings - Support for path-style URLs (MinIO) and virtual-hosted-style URLs (AWS S3) - Automatic SSL detection based on port 443 in endpoint - Separate port handling in S3 API requests - Modified `Cdn_Core.php` to pass `storage_type` to engine configuration - Updated `CdnEngine_S3_Compatible.php`: - Proper SSL detection from configuration - Automatic SSL detection by port 443 - Pass `storage_type` parameter to S3Compatible class - Updated `lib/S3Compatible.php`: - Store port separately from hostname (`$endpointPort` static property) - Store storage type (`$storageType` static property) - URL style determination based on `storage_type`: - `aws`: virtual-hosted-style (bucket.host/path) - `minio`: path-style (host:port/bucket/path) - `auto`: automatic detection by endpoint domain - Proper port handling in URL construction (without duplication) - Proper Host header formation (without port for virtual-hosted-style) - Fixed 404 error when uploading to MinIO (incorrect URL style) - Fixed 400 error when uploading to MinIO (incorrect Host header and path construction) - Fixed parse error in `S3Compatible.php` after applying patches - Added `setStorageType()` static method to `S3Compatible` class - Modified `S3Request` constructor to use `storage_type` for URL style determination - Updated `getResponse()` method to include port in URL when needed - Port extraction from endpoint in `__construct()` and `setEndpoint()` methods --- CdnEngine_S3_Compatible.php | 32 ++++++++++-- Cdn_Core.php | 3 +- ConfigKeys.php | 4 ++ inc/options/cdn/s3_compatible.php | 19 +++++++ lib/S3Compatible.php | 85 +++++++++++++++++++++++++++++-- 5 files changed, 132 insertions(+), 11 deletions(-) diff --git a/CdnEngine_S3_Compatible.php b/CdnEngine_S3_Compatible.php index c1b8059ad..c9cee4de5 100644 --- a/CdnEngine_S3_Compatible.php +++ b/CdnEngine_S3_Compatible.php @@ -39,15 +39,37 @@ class CdnEngine_S3_Compatible extends CdnEngine_Base { public function __construct( $config = array() ) { $config = array_merge( array( - 'key' => '', - 'secret' => '', - 'bucket' => '', - 'cname' => array(), + 'key' => '', + 'secret' => '', + 'bucket' => '', + 'cname' => array(), + 'ssl' => 'auto', + 'storage_type' => 'auto', ), $config ); - $this->_s3 = new \S3Compatible( $config['key'], $config['secret'], false, $config['api_host'] ); + // Determine SSL usage from config + $useSSL = false; + if ( isset( $config['ssl'] ) ) { + if ( $config['ssl'] === 'enabled' ) { + $useSSL = true; + } elseif ( $config['ssl'] === 'auto' ) { + // Auto-detect: use SSL if endpoint contains port 443 + $api_host = isset( $config['api_host'] ) ? $config['api_host'] : ''; + $useSSL = ( strpos( $api_host, ':443' ) !== false ); + } + } + + // Extract hostname and port from api_host + $api_host = isset( $config['api_host'] ) ? $config['api_host'] : ''; + // Pass full endpoint with port - S3Compatible will extract port for Host header + $endpoint = $api_host; + + // Pass storage_type to S3Compatible for URL format determination + $storage_type = isset( $config['storage_type'] ) ? $config['storage_type'] : 'auto'; + + $this->_s3 = new \S3Compatible( $config['key'], $config['secret'], $useSSL, $endpoint, '', $storage_type ); $this->_s3->setSignatureVersion( 'v2' ); parent::__construct( $config ); diff --git a/Cdn_Core.php b/Cdn_Core.php index 518ec8cda..21b52d2f4 100644 --- a/Cdn_Core.php +++ b/Cdn_Core.php @@ -565,7 +565,8 @@ public function get_cdn() { 'cname' => $c->get_array( 'cdn.s3.cname' ), 'ssl' => $c->get_string( 'cdn.s3.ssl' ), 'compression' => $compression, - 'api_host' => $c->get_string( 'cdn.s3_compatible.api_host' ), + 'api_host' => $c->get_string( 'cdn.s3_compatible.api_host' ), + 'storage_type' => $c->get_string( 'cdn.s3_compatible.storage_type' ), ); break; diff --git a/ConfigKeys.php b/ConfigKeys.php index b74d4c606..1c4a3c020 100644 --- a/ConfigKeys.php +++ b/ConfigKeys.php @@ -1358,6 +1358,10 @@ 'type' => 'string', 'default' => 'auto', ), + 'cdn.s3_compatible.storage_type' => array( + 'type' => 'string', + 'default' => 'auto', + ), 'cdn.cf.key' => array( 'type' => 'string', 'default' => '', diff --git a/inc/options/cdn/s3_compatible.php b/inc/options/cdn/s3_compatible.php index 489257574..06b1a63e3 100644 --- a/inc/options/cdn/s3_compatible.php +++ b/inc/options/cdn/s3_compatible.php @@ -14,6 +14,25 @@ 'description' => esc_html__( 'Host of API endpoint, comptabile with Amazon S3 API', 'w3-total-cache' ), ) ); +?> + + + + + + +

+ +

+ + + 'cdn.s3.key', diff --git a/lib/S3Compatible.php b/lib/S3Compatible.php index bd633a6aa..8c14c6838 100644 --- a/lib/S3Compatible.php +++ b/lib/S3Compatible.php @@ -73,6 +73,24 @@ class S3Compatible */ public static $endpoint = 's3.amazonaws.com'; + /** + * Endpoint port (if specified) + * + * @var string + * @access public + * @static + */ + public static $endpointPort = ''; + + /** + * Storage type: 'auto', 'aws', or 'minio' + * + * @var string + * @access public + * @static + */ + public static $storageType = 'auto'; + /** * AWS Region * @@ -195,15 +213,28 @@ class S3Compatible * @param string $secretKey Secret key * @param boolean $useSSL Enable SSL * @param string $endpoint Amazon URI + * @param string $region AWS region + * @param string $storageType Storage type: 'auto', 'aws', or 'minio' * @return void */ - public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '') + public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '', $storageType = 'auto') { if ($accessKey !== null && $secretKey !== null) self::setAuth($accessKey, $secretKey); self::$useSSL = $useSSL; - self::$endpoint = $endpoint; + + // Extract port from endpoint if present + if (strpos($endpoint, ':') !== false) { + $parts = explode(':', $endpoint, 2); + self::$endpoint = $parts[0]; + self::$endpointPort = ':' . $parts[1]; + } else { + self::$endpoint = $endpoint; + self::$endpointPort = ''; + } + self::$region = $region; + self::$storageType = $storageType; } @@ -215,7 +246,26 @@ public function __construct($accessKey = null, $secretKey = null, $useSSL = fals */ public function setEndpoint($host) { - self::$endpoint = $host; + // Extract port from endpoint if present + if (strpos($host, ':') !== false) { + $parts = explode(':', $host, 2); + self::$endpoint = $parts[0]; + self::$endpointPort = ':' . $parts[1]; + } else { + self::$endpoint = $host; + self::$endpointPort = ''; + } + } + + /** + * Set the storage type + * + * @param string $type Storage type: 'auto', 'aws', or 'minio' + * @return void + */ + public static function setStorageType($type) + { + self::$storageType = $type; } @@ -866,15 +916,33 @@ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.c $this->bucket = $bucket; $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; + // Determine URL style based on storage_type setting + $useVirtualHostedStyle = false; + $storageType = isset(\S3Compatible::$storageType) ? \S3Compatible::$storageType : 'auto'; + if ($storageType === 'aws') { + // Explicitly set to AWS - use virtual-hosted-style if bucket name is valid + $useVirtualHostedStyle = $this->dnsBucketName($this->bucket); + } elseif ($storageType === 'minio') { + // Explicitly set to MinIO - always use path-style + $useVirtualHostedStyle = false; + } else { + // Auto-detect: determine if this is an AWS endpoint + $isAWS = (strpos($endpoint, 'amazonaws.com') !== false || strpos($endpoint, 's3.') === 0); + $useVirtualHostedStyle = ($isAWS && $this->dnsBucketName($this->bucket)); + } + if ($this->bucket !== '') { - if ($this->dnsBucketName($this->bucket)) + // Use virtual-hosted-style for AWS if enabled and bucket name is DNS-compatible + // Use path-style for MinIO or invalid bucket names + if ($useVirtualHostedStyle) { $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; $this->resource = '/'.$this->bucket.$this->uri; } else { + // Path-style: bucket in URI path $this->headers['Host'] = $this->endpoint; if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; $this->bucket = ''; @@ -959,7 +1027,14 @@ public function getResponse() array_key_exists('logging', $this->parameters)) $this->resource .= $query; } - $url = (S3Compatible::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; + // Build URL with port if specified + $host = ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint); + $port = isset(\S3Compatible::$endpointPort) ? \S3Compatible::$endpointPort : ''; + // Don't add port to URL if host already contains port (for virtual-hosted-style) + if (strpos($host, ':') !== false) { + $port = ''; + } + $url = (S3Compatible::$useSSL ? 'https://' : 'http://') . $host . $port . $this->uri; // Basic setup $curl = curl_init();