diff --git a/composer.json b/composer.json index 2f76ce4..63d8301 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,14 @@ { "name": "brasdrive/flysystem-offload", - "description": "Universal storage offloading for WordPress vía Flysystem", + "description": "Universal storage offloading for WordPress via Flysystem", "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Brasdrive", + "email": "jdavidcamejo@gmail.com" + } + ], "require": { "php": ">=8.1", "league/flysystem": "^3.24", @@ -16,9 +23,36 @@ "azure-oss/storage-blob-flysystem": "^1.3.0", "sabre/dav": "^4.5" }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "^3.7" + }, "autoload": { "psr-4": { "FlysystemOffload\\": "src/" } - } + }, + "autoload-dev": { + "psr-4": { + "FlysystemOffload\\Tests\\": "tests/" + } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true, + "allow-plugins": { + "composer/installers": true + }, + "platform": { + "php": "8.1" + } + }, + "scripts": { + "test": "phpunit", + "cs": "phpcs --standard=PSR12 src/", + "cbf": "phpcbf --standard=PSR12 src/", + "dump": "composer dump-autoload --optimize" + }, + "minimum-stability": "stable", + "prefer-stable": true } diff --git a/src/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index 291aa9f..91573b8 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -1,58 +1,530 @@ configDirectory = rtrim($configDirectory, '/\\'); + // Cargar desde opciones de WordPress + $dbConfig = self::loadFromDatabase(); + + // Merge con prioridad a la base de datos sobre el archivo + // Si hay configuración en BD, usar esa; si no, usar archivo + $config = !empty($dbConfig['provider']) + ? array_merge($fileConfig, $dbConfig) + : $fileConfig; + + // Normalizar configuración + return self::normalize($config); } - public function load(): array { - $candidateFiles = [ - $this->configDirectory . '/flysystem-offload.local.php', - $this->configDirectory . '/flysystem-offload.php', - $this->configDirectory . '/flysystem-offload.example.php', - ]; + /** + * Carga configuración desde archivo PHP + * + * @return array + */ + private static function loadFromFile(): array + { + $configFile = defined('FLYSYSTEM_OFFLOAD_CONFIG_PATH') + ? FLYSYSTEM_OFFLOAD_CONFIG_PATH . '/flysystem-offload.php' + : ''; - $configFile = $this->resolveFirstExisting($candidateFiles); - - if ($configFile === null) { - throw new RuntimeException( - sprintf( - 'No se pudo localizar un archivo de configuración para Flysystem Offload. Esperado en: %s', - implode(', ', $candidateFiles) - ) - ); + if (empty($configFile) || !file_exists($configFile)) { + error_log('[Flysystem Offload] Config file not found: ' . $configFile); + return []; } - $config = include $configFile; + $rawConfig = require $configFile; - if ($config instanceof \Closure) { - $config = $config(); + if (!is_array($rawConfig)) { + error_log('[Flysystem Offload] Config file must return an array'); + return []; } - if (! is_array($config)) { - throw new UnexpectedValueException( - sprintf('El archivo de configuración debe retornar un array. Archivo: %s', $configFile) - ); + error_log('[Flysystem Offload] Loaded config from file: ' . print_r($rawConfig, true)); + + // Convertir estructura del archivo al formato esperado + return self::normalizeFileConfig($rawConfig); + } + + /** + * Normaliza la configuración del archivo al formato esperado + * + * @param array $rawConfig Configuración raw del archivo + * @return array Configuración normalizada + */ + private static function normalizeFileConfig(array $rawConfig): array + { + $config = []; + + // Convertir 'driver' a 'provider' + if (isset($rawConfig['driver'])) { + $config['provider'] = $rawConfig['driver']; } + // Extraer prefijo global si existe + if (isset($rawConfig['stream']['root_prefix'])) { + $config['prefix'] = $rawConfig['stream']['root_prefix']; + } + + // Normalizar configuración según el driver/provider + $provider = $config['provider'] ?? ''; + + switch ($provider) { + case 's3': + if (isset($rawConfig['s3'])) { + $s3Config = $rawConfig['s3']; + $config['key'] = $s3Config['key'] ?? ''; + $config['secret'] = $s3Config['secret'] ?? ''; + $config['region'] = $s3Config['region'] ?? 'us-east-1'; + $config['bucket'] = $s3Config['bucket'] ?? ''; + $config['endpoint'] = $s3Config['endpoint'] ?? ''; + $config['use_path_style_endpoint'] = $s3Config['use_path_style_endpoint'] ?? false; + + // Usar prefix específico de S3 si existe, sino el global + if (isset($s3Config['prefix'])) { + $config['prefix'] = $s3Config['prefix']; + } + + // CDN URL desde uploads.base_url + if (isset($rawConfig['uploads']['base_url'])) { + $config['cdn_url'] = $rawConfig['uploads']['base_url']; + } + } + break; + + case 'webdav': + if (isset($rawConfig['webdav'])) { + $webdavConfig = $rawConfig['webdav']; + + // Determinar base_uri desde endpoint o base_url + $config['base_uri'] = $webdavConfig['endpoint'] + ?? $webdavConfig['base_url'] + ?? ''; + + // Credenciales + if (isset($webdavConfig['credentials'])) { + $config['username'] = $webdavConfig['credentials']['username'] ?? ''; + $config['password'] = $webdavConfig['credentials']['password'] ?? ''; + $config['auth_type'] = $webdavConfig['credentials']['auth_type'] ?? 'basic'; + } + + // Prefix específico de WebDAV + if (isset($webdavConfig['prefix'])) { + $config['prefix'] = $webdavConfig['prefix']; + } + + // Permisos (usar valores por defecto si no están definidos) + $config['file_public'] = '0644'; + $config['file_private'] = '0600'; + $config['dir_public'] = '0755'; + $config['dir_private'] = '0700'; + + // Visibilidad por defecto + if (isset($webdavConfig['default_visibility'])) { + $config['default_visibility'] = $webdavConfig['default_visibility']; + } + } + break; + + case 'sftp': + if (isset($rawConfig['sftp'])) { + $sftpConfig = $rawConfig['sftp']; + $config['host'] = $sftpConfig['host'] ?? ''; + $config['port'] = $sftpConfig['port'] ?? 22; + $config['username'] = $sftpConfig['username'] ?? ''; + $config['password'] = $sftpConfig['password'] ?? ''; + $config['private_key'] = $sftpConfig['private_key'] ?? ''; + $config['passphrase'] = $sftpConfig['passphrase'] ?? ''; + $config['root'] = $sftpConfig['root'] ?? '/'; + $config['timeout'] = $sftpConfig['timeout'] ?? 10; + + // Permisos + $config['file_public'] = $sftpConfig['file_public'] ?? '0644'; + $config['file_private'] = $sftpConfig['file_private'] ?? '0600'; + $config['dir_public'] = $sftpConfig['dir_public'] ?? '0755'; + $config['dir_private'] = $sftpConfig['dir_private'] ?? '0700'; + } + break; + } + + error_log('[Flysystem Offload] Normalized file config: ' . print_r($config, true)); + return $config; } - private function resolveFirstExisting(array $files): ?string { - foreach ($files as $file) { - if ($file && is_readable($file)) { - return $file; + /** + * Carga configuración desde la base de datos de WordPress + * + * @return array + */ + private static function loadFromDatabase(): array + { + $provider = get_option('flysystem_offload_provider', ''); + + if (empty($provider)) { + error_log('[Flysystem Offload] No provider found in database'); + return []; + } + + error_log('[Flysystem Offload] Loading config from database for provider: ' . $provider); + + $config = [ + 'provider' => $provider, + 'prefix' => get_option('flysystem_offload_prefix', ''), + ]; + + // Cargar configuración específica del proveedor + switch ($provider) { + case 's3': + $config = array_merge($config, self::loadS3Config()); + break; + + case 'webdav': + $config = array_merge($config, self::loadWebdavConfig()); + break; + + case 'sftp': + $config = array_merge($config, self::loadSftpConfig()); + break; + + case 'gcs': + $config = array_merge($config, self::loadGcsConfig()); + break; + + case 'azure': + $config = array_merge($config, self::loadAzureConfig()); + break; + + case 'dropbox': + $config = array_merge($config, self::loadDropboxConfig()); + break; + + case 'google-drive': + $config = array_merge($config, self::loadGoogleDriveConfig()); + break; + + case 'onedrive': + $config = array_merge($config, self::loadOneDriveConfig()); + break; + } + + error_log('[Flysystem Offload] Database config loaded: ' . print_r($config, true)); + + return $config; + } + + /** + * Carga configuración de S3 + * + * @return array + */ + private static function loadS3Config(): array + { + return [ + 'key' => get_option('flysystem_offload_s3_key', ''), + 'secret' => get_option('flysystem_offload_s3_secret', ''), + 'region' => get_option('flysystem_offload_s3_region', 'us-east-1'), + 'bucket' => get_option('flysystem_offload_s3_bucket', ''), + 'endpoint' => get_option('flysystem_offload_s3_endpoint', ''), + 'use_path_style_endpoint' => (bool) get_option('flysystem_offload_s3_path_style', false), + 'cdn_url' => get_option('flysystem_offload_s3_cdn_url', ''), + ]; + } + + /** + * Carga configuración de WebDAV + * + * @return array + */ + private static function loadWebdavConfig(): array + { + return [ + 'base_uri' => get_option('flysystem_offload_webdav_base_uri', ''), + 'username' => get_option('flysystem_offload_webdav_username', ''), + 'password' => get_option('flysystem_offload_webdav_password', ''), + 'auth_type' => get_option('flysystem_offload_webdav_auth_type', 'basic'), + 'prefix' => get_option('flysystem_offload_webdav_prefix', ''), + 'file_public' => self::normalizePermissionFromDb( + get_option('flysystem_offload_webdav_file_public', '0644') + ), + 'file_private' => self::normalizePermissionFromDb( + get_option('flysystem_offload_webdav_file_private', '0600') + ), + 'dir_public' => self::normalizePermissionFromDb( + get_option('flysystem_offload_webdav_dir_public', '0755') + ), + 'dir_private' => self::normalizePermissionFromDb( + get_option('flysystem_offload_webdav_dir_private', '0700') + ), + ]; + } + + /** + * Carga configuración de SFTP + * + * @return array + */ + private static function loadSftpConfig(): array + { + return [ + 'host' => get_option('flysystem_offload_sftp_host', ''), + 'port' => (int) get_option('flysystem_offload_sftp_port', 22), + 'username' => get_option('flysystem_offload_sftp_username', ''), + 'password' => get_option('flysystem_offload_sftp_password', ''), + 'private_key' => get_option('flysystem_offload_sftp_private_key', ''), + 'passphrase' => get_option('flysystem_offload_sftp_passphrase', ''), + 'root' => get_option('flysystem_offload_sftp_root', '/'), + 'timeout' => (int) get_option('flysystem_offload_sftp_timeout', 10), + 'file_public' => self::normalizePermissionFromDb( + get_option('flysystem_offload_sftp_file_public', '0644') + ), + 'file_private' => self::normalizePermissionFromDb( + get_option('flysystem_offload_sftp_file_private', '0600') + ), + 'dir_public' => self::normalizePermissionFromDb( + get_option('flysystem_offload_sftp_dir_public', '0755') + ), + 'dir_private' => self::normalizePermissionFromDb( + get_option('flysystem_offload_sftp_dir_private', '0700') + ), + ]; + } + + /** + * Carga configuración de Google Cloud Storage + * + * @return array + */ + private static function loadGcsConfig(): array + { + return [ + 'project_id' => get_option('flysystem_offload_gcs_project_id', ''), + 'key_file' => get_option('flysystem_offload_gcs_key_file', ''), + 'bucket' => get_option('flysystem_offload_gcs_bucket', ''), + ]; + } + + /** + * Carga configuración de Azure Blob Storage + * + * @return array + */ + private static function loadAzureConfig(): array + { + return [ + 'account_name' => get_option('flysystem_offload_azure_account_name', ''), + 'account_key' => get_option('flysystem_offload_azure_account_key', ''), + 'container' => get_option('flysystem_offload_azure_container', ''), + ]; + } + + /** + * Carga configuración de Dropbox + * + * @return array + */ + private static function loadDropboxConfig(): array + { + return [ + 'access_token' => get_option('flysystem_offload_dropbox_access_token', ''), + ]; + } + + /** + * Carga configuración de Google Drive + * + * @return array + */ + private static function loadGoogleDriveConfig(): array + { + return [ + 'client_id' => get_option('flysystem_offload_gdrive_client_id', ''), + 'client_secret' => get_option('flysystem_offload_gdrive_client_secret', ''), + 'refresh_token' => get_option('flysystem_offload_gdrive_refresh_token', ''), + ]; + } + + /** + * Carga configuración de OneDrive + * + * @return array + */ + private static function loadOneDriveConfig(): array + { + return [ + 'client_id' => get_option('flysystem_offload_onedrive_client_id', ''), + 'client_secret' => get_option('flysystem_offload_onedrive_client_secret', ''), + 'refresh_token' => get_option('flysystem_offload_onedrive_refresh_token', ''), + ]; + } + + /** + * Normaliza un permiso desde la base de datos + * Mantiene como string para que el adaptador lo convierta correctamente + * + * @param mixed $permission Permiso desde la BD + * @return string|int Permiso normalizado + */ + private static function normalizePermissionFromDb($permission) + { + if (is_int($permission)) { + return $permission; + } + + if (is_string($permission)) { + $permission = trim($permission); + + // Si ya tiene el formato correcto, retornar + if (preg_match('/^0[0-7]{3}$/', $permission)) { + return $permission; + } + + // Si es solo dígitos sin el 0 inicial, añadirlo + if (preg_match('/^[0-7]{3}$/', $permission)) { + return '0' . $permission; } } - return null; + // Valor por defecto + return '0644'; + } + + /** + * Normaliza la configuración completa + * + * @param array $config Configuración a normalizar + * @return array Configuración normalizada + */ + private static function normalize(array $config): array + { + // Eliminar valores vacíos excepto 0 y false + $config = array_filter($config, function ($value) { + return $value !== '' && $value !== null && $value !== []; + }); + + // Normalizar provider + if (isset($config['provider'])) { + $config['provider'] = strtolower(trim($config['provider'])); + } + + // Normalizar prefix (eliminar barras al inicio y final) + if (isset($config['prefix'])) { + $config['prefix'] = trim($config['prefix'], '/'); + } + + error_log('[Flysystem Offload] Final normalized config: ' . print_r($config, true)); + + return $config; + } + + /** + * Valida que la configuración sea válida + * + * @param array $config Configuración a validar + * @return bool + * @throws InvalidArgumentException Si la configuración es inválida + */ + public static function validate(array $config): bool + { + if (empty($config['provider'])) { + throw new InvalidArgumentException('Provider is required'); + } + + $provider = $config['provider']; + + error_log('[Flysystem Offload] Validating config for provider: ' . $provider); + + // Validar configuración específica del proveedor + switch ($provider) { + case 's3': + self::validateS3Config($config); + break; + + case 'webdav': + self::validateWebdavConfig($config); + break; + + case 'sftp': + self::validateSftpConfig($config); + break; + } + + error_log('[Flysystem Offload] Config validation passed'); + + return true; + } + + /** + * Valida configuración de S3 + * + * @param array $config + * @throws InvalidArgumentException + */ + private static function validateS3Config(array $config): void + { + $required = ['key', 'secret', 'region', 'bucket']; + + foreach ($required as $key) { + if (empty($config[$key])) { + throw new InvalidArgumentException("S3 {$key} is required"); + } + } + } + + /** + * Valida configuración de WebDAV + * + * @param array $config + * @throws InvalidArgumentException + */ + private static function validateWebdavConfig(array $config): void + { + if (empty($config['base_uri'])) { + throw new InvalidArgumentException('WebDAV base_uri is required'); + } + + if (!filter_var($config['base_uri'], FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException('WebDAV base_uri must be a valid URL'); + } + } + + /** + * Valida configuración de SFTP + * + * @param array $config + * @throws InvalidArgumentException + */ + private static function validateSftpConfig(array $config): void + { + if (empty($config['host'])) { + throw new InvalidArgumentException('SFTP host is required'); + } + + if (empty($config['username'])) { + throw new InvalidArgumentException('SFTP username is required'); + } + + if (empty($config['password']) && empty($config['private_key'])) { + throw new InvalidArgumentException( + 'SFTP password or private_key is required' + ); + } } } diff --git a/src/Filesystem/AdapterInterface.php b/src/Filesystem/AdapterInterface.php index 2501cf7..105d4f5 100644 --- a/src/Filesystem/AdapterInterface.php +++ b/src/Filesystem/AdapterInterface.php @@ -1,10 +1,39 @@ -validateConfig($config); - $webdav = $config['webdav']; - - $endpoint = $this->requiredString($webdav, 'endpoint', 'webdav.endpoint'); - $credentials = $webdav['credentials'] ?? []; - - $username = $this->requiredString($credentials, 'username', 'webdav.credentials.username'); - $password = $this->requiredString($credentials, 'password', 'webdav.credentials.password'); + // Normalizar permisos de string a integer octal + $filePublic = $this->normalizePermission($config['file_public'] ?? '0644'); + $filePrivate = $this->normalizePermission($config['file_private'] ?? '0600'); + $dirPublic = $this->normalizePermission($config['dir_public'] ?? '0755'); + $dirPrivate = $this->normalizePermission($config['dir_private'] ?? '0700'); + // Configurar cliente WebDAV $clientConfig = [ - 'baseUri' => $this->normaliseEndpoint($endpoint), - 'userName' => $username, - 'password' => $password, + 'baseUri' => rtrim($config['base_uri'], '/') . '/', ]; - $authType = $credentials['auth_type'] ?? null; - if (is_string($authType) && $authType !== '') { - $clientConfig['authType'] = $authType; + // Añadir autenticación si está configurada + if (!empty($config['username'])) { + $clientConfig['userName'] = $config['username']; + $clientConfig['password'] = $config['password'] ?? ''; + $clientConfig['authType'] = $this->normalizeAuthType($config['auth_type'] ?? 'basic'); } - if (! empty($webdav['default_headers']) && is_array($webdav['default_headers'])) { - $clientConfig['headers'] = $this->normaliseHeaders($webdav['default_headers']); - } - - if (! empty($webdav['timeout'])) { - $clientConfig['timeout'] = (int) $webdav['timeout']; - } - - if (! empty($webdav['curl_options']) && is_array($webdav['curl_options'])) { - $clientConfig['curl.options'] = $webdav['curl_options']; + // Configuración adicional opcional + if (isset($config['encoding'])) { + $clientConfig['encoding'] = $config['encoding']; } $client = new Client($clientConfig); - $prefix = ''; - if (! empty($webdav['prefix'])) { - $prefix = trim((string) $webdav['prefix'], '/'); - } - - $defaultVisibility = $webdav['default_visibility'] ?? ($config['visibility'] ?? Visibility::PRIVATE); - $visibility = $this->normaliseVisibility($defaultVisibility); - - $permissions = $this->normalisePermissions($webdav['permissions'] ?? []); - - $directoryDefault = $visibility === Visibility::PUBLIC ? 0755 : 0700; - - $visibilityConverter = PortableVisibilityConverter::fromArray( - $permissions, - $directoryDefault + // Crear convertidor de visibilidad con permisos normalizados + $visibility = new PortableVisibilityConverter( + $filePublic, + $filePrivate, + $dirPublic, + $dirPrivate, + Visibility::PRIVATE // Visibilidad por defecto ); - return new LeagueWebDAVAdapter($client, $prefix, $visibilityConverter); + // Crear y retornar el adaptador + return new FlysystemWebDAVAdapter( + $client, + $config['prefix'] ?? '', + $visibility + ); } - private function requiredString(array $config, string $key, ?string $path = null): string + /** + * Convierte permisos de string octal a integer + * + * @param mixed $permission Permiso en formato string u octal + * @return int Permiso como integer octal + * @throws InvalidArgumentException Si el formato es inválido + */ + private function normalizePermission($permission): int { - $value = $config[$key] ?? null; - - if (! is_string($value) || trim($value) === '') { - $path ??= $key; - - throw new InvalidArgumentException(sprintf( - 'El valor "%s" es obligatorio en la configuración de WebDAV.', - $path - )); + // Si ya es integer, retornar directamente + if (is_int($permission)) { + return $permission; } - return $value; - } - - private function normaliseEndpoint(string $endpoint): string - { - return rtrim(trim($endpoint), '/') . '/'; - } - - private function normaliseHeaders(array $headers): array - { - $normalised = []; - - foreach ($headers as $header => $value) { - if (! is_string($header) || $header === '') { - continue; + // Si es string, convertir de octal a decimal + if (is_string($permission)) { + // Limpiar espacios + $permission = trim($permission); + + // Si comienza con '0', es octal + if (str_starts_with($permission, '0')) { + $result = octdec($permission); + if ($result === false || $result === 0 && $permission !== '0' && $permission !== '0000') { + throw new InvalidArgumentException( + "Invalid octal permission format: {$permission}" + ); + } + return $result; } - - $normalised[$header] = is_array($value) ? implode(',', $value) : (string) $value; + + // Si es decimal como '644', añadir el 0 y convertir + if (ctype_digit($permission)) { + return octdec('0' . $permission); + } + + throw new InvalidArgumentException( + "Permission must be an octal string (e.g., '0644') or integer" + ); } - return $normalised; + throw new InvalidArgumentException( + 'Permission must be an integer or string, ' . gettype($permission) . ' given' + ); } - private function normaliseVisibility(string $visibility): string + /** + * Normaliza el tipo de autenticación + * + * @param string|int $authType Tipo de autenticación + * @return int Constante de autenticación de Sabre\DAV\Client + */ + private function normalizeAuthType($authType): int { - return strtolower($visibility) === Visibility::PUBLIC - ? Visibility::PUBLIC - : Visibility::PRIVATE; + if (is_int($authType)) { + return $authType; + } + + $authType = strtolower(trim($authType)); + + return match ($authType) { + 'basic' => Client::AUTH_BASIC, + 'digest' => Client::AUTH_DIGEST, + 'ntlm' => Client::AUTH_NTLM, + default => Client::AUTH_BASIC, + }; } - private function normalisePermissions(array $permissions): array + /** + * Valida la configuración del adaptador + * + * @param array $config Configuración a validar + * @throws InvalidArgumentException Si falta configuración requerida + */ + private function validateConfig(array $config): void { - $defaults = [ - 'file' => [ - Visibility::PUBLIC => 0644, - Visibility::PRIVATE => 0600, - ], - 'dir' => [ - Visibility::PUBLIC => 0755, - Visibility::PRIVATE => 0700, - ], + // Validar base_uri requerido + if (empty($config['base_uri'])) { + throw new InvalidArgumentException('WebDAV base_uri is required'); + } + + // Validar formato de URL + if (!filter_var($config['base_uri'], FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException( + 'Invalid WebDAV base_uri format. Must be a valid URL (e.g., https://webdav.example.com/remote.php/dav/files/username/)' + ); + } + + // Validar que sea HTTP o HTTPS + $scheme = parse_url($config['base_uri'], PHP_URL_SCHEME); + if (!in_array($scheme, ['http', 'https'], true)) { + throw new InvalidArgumentException( + 'WebDAV base_uri must use http:// or https:// scheme' + ); + } + + // Si hay username, debe haber password + if (!empty($config['username']) && !isset($config['password'])) { + throw new InvalidArgumentException( + 'WebDAV password is required when username is provided' + ); + } + } + + /** + * Obtiene las claves de configuración requeridas + * + * @return array + */ + public function getRequiredConfigKeys(): array + { + return ['base_uri']; + } + + /** + * Obtiene las claves de configuración opcionales + * + * @return array + */ + public function getOptionalConfigKeys(): array + { + return [ + 'username', + 'password', + 'auth_type', + 'prefix', + 'file_public', + 'file_private', + 'dir_public', + 'dir_private', + 'encoding', ]; + } - foreach (['file', 'dir'] as $type) { - if (! isset($permissions[$type]) || ! is_array($permissions[$type])) { - continue; - } + /** + * Obtiene la descripción del adaptador + * + * @return string + */ + public function getDescription(): string + { + return 'WebDAV storage adapter for Nextcloud, ownCloud, and other WebDAV-compatible servers'; + } - foreach ($permissions[$type] as $visibility => $mode) { - $visibility = strtolower((string) $visibility); - - if ($visibility !== Visibility::PUBLIC && $visibility !== Visibility::PRIVATE) { - continue; - } - - if (is_string($mode)) { - $mode = trim($mode); - - if ($mode === '') { - continue; - } - - $mode = octdec($mode); - } elseif (! is_int($mode)) { - continue; - } - - $defaults[$type][$visibility] = $mode; - } - } - - return $defaults; + /** + * Obtiene valores por defecto para la configuración + * + * @return array + */ + public function getDefaultConfig(): array + { + return [ + 'auth_type' => 'basic', + 'prefix' => '', + 'file_public' => '0644', + 'file_private' => '0600', + 'dir_public' => '0755', + 'dir_private' => '0700', + ]; } } diff --git a/src/Filesystem/FilesystemFactory.php b/src/Filesystem/FilesystemFactory.php index e3c4ed5..1d786d3 100644 --- a/src/Filesystem/FilesystemFactory.php +++ b/src/Filesystem/FilesystemFactory.php @@ -4,31 +4,232 @@ declare(strict_types=1); namespace FlysystemOffload\Filesystem; +use FlysystemOffload\Filesystem\Adapters\AdapterInterface; use FlysystemOffload\Filesystem\Adapters\S3Adapter; use FlysystemOffload\Filesystem\Adapters\WebdavAdapter; +use FlysystemOffload\Filesystem\Adapters\SftpAdapter; +use FlysystemOffload\Filesystem\Adapters\GoogleCloudAdapter; +use FlysystemOffload\Filesystem\Adapters\AzureBlobAdapter; +use FlysystemOffload\Filesystem\Adapters\DropboxAdapter; +use FlysystemOffload\Filesystem\Adapters\GoogleDriveAdapter; +use FlysystemOffload\Filesystem\Adapters\OneDriveAdapter; +use FlysystemOffload\Filesystem\Adapters\PrefixedAdapter; use League\Flysystem\Filesystem; -use League\Flysystem\FilesystemOperator; -use League\Flysystem\Visibility; +use League\Flysystem\FilesystemAdapter; +use InvalidArgumentException; -final class FilesystemFactory +/** + * Factory para crear instancias de Filesystem con diferentes adaptadores + */ +class FilesystemFactory { - public function make(array $config): FilesystemOperator + /** + * Mapa de proveedores a clases de adaptadores + */ + private const ADAPTER_MAP = [ + 's3' => S3Adapter::class, + 'webdav' => WebdavAdapter::class, + 'sftp' => SftpAdapter::class, + 'gcs' => GoogleCloudAdapter::class, + 'azure' => AzureBlobAdapter::class, + 'dropbox' => DropboxAdapter::class, + 'google-drive' => GoogleDriveAdapter::class, + 'onedrive' => OneDriveAdapter::class, + ]; + + /** + * Crea un Filesystem basado en la configuración proporcionada + * + * @param array $config Configuración del filesystem + * @return Filesystem + * @throws InvalidArgumentException Si el proveedor no es válido + */ + public static function create(array $config): Filesystem { - $driver = $config['driver'] ?? 's3'; + $provider = $config['provider'] ?? ''; - $adapter = match ($driver) { - 's3' => (new S3Adapter())->createAdapter($config), - 'webdav' => (new WebdavAdapter())->createAdapter($config), - default => throw new \InvalidArgumentException( - sprintf('Driver de Flysystem no soportado: "%s".', $driver) - ), - }; + if (empty($provider)) { + throw new InvalidArgumentException('Provider is required in configuration'); + } - $filesystemConfig = [ - 'visibility' => $config['visibility'] ?? Visibility::PUBLIC, - 'directory_visibility' => $config['visibility'] ?? Visibility::PUBLIC, + $adapter = self::createAdapter($provider, $config); + + // Aplicar prefijo global si está configurado + if (!empty($config['prefix'])) { + $adapter = new PrefixedAdapter($adapter, $config['prefix']); + } + + return new Filesystem($adapter); + } + + /** + * Crea un adaptador específico basado en el proveedor + * + * @param string $provider Nombre del proveedor + * @param array $config Configuración del adaptador + * @return FilesystemAdapter + * @throws InvalidArgumentException Si el proveedor no es soportado + */ + private static function createAdapter(string $provider, array $config): FilesystemAdapter + { + $provider = strtolower(trim($provider)); + + if (!isset(self::ADAPTER_MAP[$provider])) { + throw new InvalidArgumentException( + sprintf( + 'Unsupported provider: %s. Supported providers: %s', + $provider, + implode(', ', array_keys(self::ADAPTER_MAP)) + ) + ); + } + + $adapterClass = self::ADAPTER_MAP[$provider]; + + if (!class_exists($adapterClass)) { + throw new InvalidArgumentException( + sprintf('Adapter class not found: %s', $adapterClass) + ); + } + + /** @var AdapterInterface $adapterInstance */ + $adapterInstance = new $adapterClass(); + + if (!$adapterInstance instanceof AdapterInterface) { + throw new InvalidArgumentException( + sprintf('Adapter must implement AdapterInterface: %s', $adapterClass) + ); + } + + // Normalizar configuración específica del proveedor + $normalizedConfig = self::normalizeConfig($provider, $config); + + return $adapterInstance->createAdapter($normalizedConfig); + } + + /** + * Normaliza la configuración según el proveedor + * + * @param string $provider Nombre del proveedor + * @param array $config Configuración original + * @return array Configuración normalizada + */ + private static function normalizeConfig(string $provider, array $config): array + { + $normalized = $config; + + // Normalizar permisos para WebDAV y SFTP + if (in_array($provider, ['webdav', 'sftp'], true)) { + $normalized = self::normalizePermissions($normalized); + } + + // Normalizar configuración específica de S3 + if ($provider === 's3') { + $normalized = self::normalizeS3Config($normalized); + } + + return $normalized; + } + + /** + * Normaliza permisos de archivos y directorios + * + * @param array $config Configuración original + * @return array Configuración con permisos normalizados + */ + private static function normalizePermissions(array $config): array + { + $permissionKeys = ['file_public', 'file_private', 'dir_public', 'dir_private']; + + foreach ($permissionKeys as $key) { + if (isset($config[$key])) { + // Si es string, mantenerlo como string (el adaptador lo convertirá) + // Si es int, mantenerlo como int + // Esto permite flexibilidad en la configuración + continue; + } + } + + // Establecer valores por defecto si no existen + $config['file_public'] = $config['file_public'] ?? 0644; + $config['file_private'] = $config['file_private'] ?? 0600; + $config['dir_public'] = $config['dir_public'] ?? 0755; + $config['dir_private'] = $config['dir_private'] ?? 0700; + + return $config; + } + + /** + * Normaliza configuración específica de S3 + * + * @param array $config Configuración original + * @return array Configuración normalizada + */ + private static function normalizeS3Config(array $config): array + { + // Asegurar que use_path_style_endpoint sea booleano + if (isset($config['use_path_style_endpoint'])) { + $config['use_path_style_endpoint'] = filter_var( + $config['use_path_style_endpoint'], + FILTER_VALIDATE_BOOLEAN + ); + } + + // Normalizar región + if (isset($config['region'])) { + $config['region'] = strtolower(trim($config['region'])); + } + + return $config; + } + + /** + * Obtiene la lista de proveedores soportados + * + * @return array + */ + public static function getSupportedProviders(): array + { + return array_keys(self::ADAPTER_MAP); + } + + /** + * Verifica si un proveedor es soportado + * + * @param string $provider Nombre del proveedor + * @return bool + */ + public static function isProviderSupported(string $provider): bool + { + return isset(self::ADAPTER_MAP[strtolower(trim($provider))]); + } + + /** + * Obtiene información sobre un proveedor específico + * + * @param string $provider Nombre del proveedor + * @return array Información del proveedor + * @throws InvalidArgumentException Si el proveedor no es soportado + */ + public static function getProviderInfo(string $provider): array + { + $provider = strtolower(trim($provider)); + + if (!self::isProviderSupported($provider)) { + throw new InvalidArgumentException("Unsupported provider: {$provider}"); + } + + $adapterClass = self::ADAPTER_MAP[$provider]; + $adapter = new $adapterClass(); + + return [ + 'name' => $provider, + 'class' => $adapterClass, + 'required_keys' => $adapter->getRequiredConfigKeys(), + 'optional_keys' => $adapter->getOptionalConfigKeys(), + 'description' => method_exists($adapter, 'getDescription') + ? $adapter->getDescription() + : '', ]; - - return new Filesystem($adapter, $filesystemConfig); } } diff --git a/src/Plugin.php b/src/Plugin.php index 5b84576..316a3a1 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,165 +1,159 @@ config = $config; - $this->filesystem = $filesystem; - $this->mediaHooks = $mediaHooks; - $this->settingsPage = $settingsPage; - $this->healthCheck = $healthCheck; - } + /** + * Indica si el plugin está inicializado + */ + private static bool $initialized = false; - public static function bootstrap(): void { - if (self::$bootstrapped) { + /** + * Bootstrap del plugin + * + * @throws Throwable + */ + public static function bootstrap(): void + { + if (self::$initialized) { return; } - $configDirectory = \defined('FLYSYSTEM_OFFLOAD_CONFIG_PATH') - ? FLYSYSTEM_OFFLOAD_CONFIG_PATH - : \dirname(__DIR__) . '/config'; + try { + // Cargar configuración + self::$config = ConfigLoader::load(); - $configLoader = new ConfigLoader($configDirectory); - $config = self::normaliseConfig($configLoader->load()); + // Validar que haya un proveedor configurado + if (empty(self::$config['provider'])) { + error_log('[Flysystem Offload] No provider configured. Please configure the plugin in Settings > Flysystem Offload'); + self::registerAdminNotice('No storage provider configured. Please configure the plugin.'); + return; + } - $filesystemFactory = new FilesystemFactory(); - $filesystem = $filesystemFactory->make($config); + // Validar configuración + ConfigLoader::validate(self::$config); - FlysystemStreamWrapper::register( - $filesystem, - $config['stream']['protocol'], - $config['stream']['root_prefix'], - $config['visibility'] - ); + // Crear filesystem + self::$filesystem = FilesystemFactory::create(self::$config); - $mediaHooks = new MediaHooks($filesystem, $config); - $mediaHooks->register(); + // Registrar stream wrapper + FlysystemStreamWrapper::register(self::$filesystem); - $settingsPage = null; - if (! empty($config['admin']['enabled']) && \is_admin()) { - $settingsPage = new SettingsPage($filesystem, $config); - $settingsPage->register(); + // Registrar hooks de medios + MediaHooks::register(self::$config); + + // Registrar página de ajustes + if (is_admin()) { + SettingsPage::register(); + } + + self::$initialized = true; + + error_log('[Flysystem Offload] Plugin initialized successfully with provider: ' . self::$config['provider']); + + } catch (Throwable $e) { + error_log('[Flysystem Offload] Initialization error: ' . $e->getMessage()); + error_log('[Flysystem Offload] Stack trace: ' . $e->getTraceAsString()); + + self::registerAdminNotice( + 'Failed to initialize: ' . $e->getMessage() + ); + + throw $e; + } + } + + /** + * Obtiene la instancia del Filesystem + * + * @return Filesystem|null + */ + public static function getFilesystem(): ?Filesystem + { + return self::$filesystem; + } + + /** + * Obtiene la configuración del plugin + * + * @return array + */ + public static function getConfig(): array + { + return self::$config; + } + + /** + * Verifica si el plugin está inicializado + * + * @return bool + */ + public static function isInitialized(): bool + { + return self::$initialized; + } + + /** + * Registra un aviso de administración + * + * @param string $message Mensaje a mostrar + * @param string $type Tipo de aviso (error, warning, info, success) + */ + private static function registerAdminNotice(string $message, string $type = 'error'): void + { + add_action('admin_notices', static function () use ($message, $type): void { + if (!current_user_can('manage_options')) { + return; + } + + printf( + '
Flysystem Offload: %s