config = $config; $this->filesystem = $filesystem; $this->mediaHooks = $mediaHooks; $this->settingsPage = $settingsPage; $this->healthCheck = $healthCheck; } public static function bootstrap(): void { if (self::$bootstrapped) { return; } $configDirectory = \defined('FLYSYSTEM_OFFLOAD_CONFIG_PATH') ? FLYSYSTEM_OFFLOAD_CONFIG_PATH : \dirname(__DIR__) . '/config'; $configLoader = new ConfigLoader($configDirectory); $config = self::normaliseConfig($configLoader->load()); $filesystemFactory = new FilesystemFactory(); $filesystem = $filesystemFactory->make($config); FlysystemStreamWrapper::register( $filesystem, $config['stream']['protocol'], $config['stream']['root_prefix'], $config['visibility'] ); $mediaHooks = new MediaHooks($filesystem, $config); $mediaHooks->register(); $settingsPage = null; if (! empty($config['admin']['enabled']) && \is_admin()) { $settingsPage = new SettingsPage($filesystem, $config); $settingsPage->register(); } $healthCheck = new HealthCheck($filesystem, $config); $healthCheck->register(); self::$instance = new self($config, $filesystem, $mediaHooks, $settingsPage, $healthCheck); self::$bootstrapped = true; } public static function instance(): ?self { return self::$instance; } public function config(): array { return $this->config; } public function filesystem(): FilesystemOperator { return $this->filesystem; } private static function normaliseConfig(array $config): array { $defaults = [ 'driver' => 's3', 'visibility' => Visibility::PUBLIC, 'cache_ttl' => 900, 'stream' => [ 'protocol' => 'flysystem', 'root_prefix' => '', 'host' => 'uploads', ], 'uploads' => [ 'base_url' => '', 'delete_remote' => true, 'prefer_local_for_missing' => false, 'cache_control' => 'public, max-age=31536000, immutable', 'expires' => null, 'expires_ttl' => 31536000, ], 'admin' => [ 'enabled' => false, ], 's3' => [ 'acl_public' => 'public-read', 'acl_private' => 'private', 'default_options' => [], ], ]; $config = array_replace_recursive($defaults, $config); $config['visibility'] = self::normaliseVisibility((string) ($config['visibility'] ?? Visibility::PUBLIC)); $config['stream']['protocol'] = self::sanitizeProtocol((string) $config['stream']['protocol']); $config['stream']['root_prefix'] = self::normalizePathSegment((string) $config['stream']['root_prefix']); $config['stream']['host'] = self::normalizePathSegment((string) $config['stream']['host']) ?: 'uploads'; if (empty($config['uploads']['base_url'])) { $config['uploads']['base_url'] = rtrim(content_url('uploads'), '/'); } else { $config['uploads']['base_url'] = rtrim((string) $config['uploads']['base_url'], '/'); } $config['uploads']['delete_remote'] = (bool) $config['uploads']['delete_remote']; $config['uploads']['prefer_local_for_missing'] = (bool) $config['uploads']['prefer_local_for_missing']; $config['uploads']['cache_control'] = trim((string) $config['uploads']['cache_control']); $config['uploads']['expires'] = $config['uploads']['expires'] ? trim((string) $config['uploads']['expires']) : null; $config['uploads']['expires_ttl'] = max(0, (int) ($config['uploads']['expires_ttl'] ?? 0)); $config['s3']['acl_public'] = (string) ($config['s3']['acl_public'] ?? 'public-read'); $config['s3']['acl_private'] = (string) ($config['s3']['acl_private'] ?? 'private'); $config['s3']['default_options'] = is_array($config['s3']['default_options']) ? $config['s3']['default_options'] : []; return $config; } private static function normaliseVisibility(string $visibility): string { $visibility = \strtolower($visibility); return $visibility === Visibility::PRIVATE ? Visibility::PRIVATE : Visibility::PUBLIC; } private static function sanitizeProtocol(string $protocol): string { $protocol = \preg_replace('/[^A-Za-z0-9_\-]/', '', $protocol) ?? 'flysystem'; $protocol = \strtolower($protocol); return $protocol !== '' ? $protocol : 'flysystem'; } private static function normalizePathSegment(string $segment): string { return \trim($segment, " \t\n\r\0\x0B/"); } }