| 1 |
<?php |
| 2 |
namespace Dudlewebs\WPMCS; |
| 3 |
|
| 4 |
defined('ABSPATH') || exit; |
| 5 |
|
| 6 |
/** |
| 7 |
* Last-resort, read-only fallback for wpmcs_get_attached_file — every |
| 8 |
* specific integration (Compatibility, EWWW, Meta Slider, all priority 10) |
| 9 |
* runs first and wins if it matches; this only engages when nothing else |
| 10 |
* has already resolved a real local path. |
| 11 |
* |
| 12 |
* @since 1.4.3 |
| 13 |
*/ |
| 14 |
class StreamWrapper { |
| 15 |
private static $instance = null; |
| 16 |
const PROTOCOL = 'wpmcssw'; |
| 17 |
private $registered = false; |
| 18 |
private $bucket_name = null; |
| 19 |
private $failed_bucket = null; |
| 20 |
|
| 21 |
public function __construct() { |
| 22 |
add_filter('wpmcs_get_attached_file', [$this, 'stream_wrapper_fallback'], 999, 4); |
| 23 |
} |
| 24 |
|
| 25 |
public function stream_wrapper_fallback($url, $file, $attachment_id, $item) { |
| 26 |
// Skip the filtered wp_get_upload_dir() call when $url is clearly still a cloud URL, not a local path. |
| 27 |
$looks_like_url = stripos((string) $url, 'http') === 0; |
| 28 |
if (!$looks_like_url) { |
| 29 |
$upload_basedir = trailingslashit(wp_get_upload_dir()['basedir']); |
| 30 |
if (strpos((string) $url, $upload_basedir) === 0 && file_exists($url)) { |
| 31 |
return $url; |
| 32 |
} |
| 33 |
} |
| 34 |
if (empty($item) || empty($item['key'])) { |
| 35 |
return $url; |
| 36 |
} |
| 37 |
if ($this->register()) { |
| 38 |
// $url always resolves to the main file's URL regardless of which WP core hook fired; $file doesn't, so use it to detect an original-image request. |
| 39 |
$key = $item['key']; |
| 40 |
if (!empty($item['original_source_path']) && !empty($item['original_key']) |
| 41 |
&& basename((string) $file) === basename($item['original_source_path'])) { |
| 42 |
$key = $item['original_key']; |
| 43 |
} |
| 44 |
return self::PROTOCOL . '://' . $this->bucket_name . '/' . $key; |
| 45 |
} |
| 46 |
// No disk-restore fallback here on purpose — that's what a dedicated integration is for. |
| 47 |
return $url; |
| 48 |
} |
| 49 |
|
| 50 |
private function register() { |
| 51 |
$bucket_name = Utils::get_credentials('bucketConfig')['bucket_name'] ?? null; |
| 52 |
$service = Utils::get_credentials('service'); |
| 53 |
if ($this->registered && $this->bucket_name === $bucket_name) { |
| 54 |
return true; |
| 55 |
} |
| 56 |
// Bucket-keyed failure memoization avoids re-attempting/re-logging on every call of a persistently broken setup. |
| 57 |
if ($this->failed_bucket === $bucket_name) { |
| 58 |
return false; |
| 59 |
} |
| 60 |
if (empty($bucket_name)) { |
| 61 |
return false; |
| 62 |
} |
| 63 |
$client = Service::instance()->get_active_client(); |
| 64 |
if (empty($client)) { |
| 65 |
$this->failed_bucket = $bucket_name; |
| 66 |
return false; |
| 67 |
} |
| 68 |
|
| 69 |
try { |
| 70 |
// Unregister explicitly first — the two vendor register() methods aren't consistently idempotent. |
| 71 |
if (in_array(self::PROTOCOL, stream_get_wrappers(), true)) { |
| 72 |
stream_wrapper_unregister(self::PROTOCOL); |
| 73 |
} |
| 74 |
// Branches on the credential value, not instanceof, to avoid force-loading the GCS SDK on non-GCS sites. |
| 75 |
if ($service === 'gcloud') { |
| 76 |
ReadOnlyGcsStreamWrapper::register($client, self::PROTOCOL); |
| 77 |
} else { |
| 78 |
ReadOnlyS3StreamWrapper::register($client, self::PROTOCOL); |
| 79 |
} |
| 80 |
} catch (\Throwable $e) { |
| 81 |
// Must never propagate — this runs inside a filter WP core fires broadly. |
| 82 |
Logger::instance()->add_log('stream_wrapper_register_failed', 0, 'media_library', ['message' => $e->getMessage()]); |
| 83 |
$this->registered = false; |
| 84 |
$this->failed_bucket = $bucket_name; |
| 85 |
return false; |
| 86 |
} |
| 87 |
|
| 88 |
// Default context ensures unaware callers get seekable reads (needed by e.g. getimagesize()). |
| 89 |
stream_context_set_default([self::PROTOCOL => ['seekable' => true]]); |
| 90 |
|
| 91 |
$this->registered = true; |
| 92 |
$this->bucket_name = $bucket_name; |
| 93 |
$this->failed_bucket = null; |
| 94 |
return true; |
| 95 |
} |
| 96 |
|
| 97 |
public static function instance() { |
| 98 |
if (is_null(self::$instance)) { |
| 99 |
self::$instance = new self(); |
| 100 |
} |
| 101 |
return self::$instance; |
| 102 |
} |
| 103 |
} |
| 104 |
|