feat: initial ACRIB WordPress deployment
- WordPress 6.9.4 (es_ES) with Kadence theme - Homepage: Hero, La Asociación, Pilares, Beneficios, Eventos, Miembros, Hazte Miembro, Contacto - Brand identity: #13294b navy, #a12932 burgundy, #c69c48 gold - Fonts: Raleway (headings) + Source Sans 3 (body) + Lato (UI) - Plugins: Kadence Blocks, Polylang, Contact Form 7 - Custom CSS with full brand styling and responsive layout - HTTPS enforced via wp-config.php proxy detection
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Thrown when we encounter issues downloading images.
|
||||
*/
|
||||
final class ImageDownloadException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Exceptions\ImageDownloadException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\ResponseAdapter;
|
||||
|
||||
final class FileNameProcessor
|
||||
{
|
||||
private array $allowed_extensions;
|
||||
|
||||
public const SCALED_SIZE = 'scaled';
|
||||
|
||||
public function __construct(
|
||||
array $allowed_extensions
|
||||
) {
|
||||
/** @var array<string, bool> */
|
||||
$this->allowed_extensions = $allowed_extensions;
|
||||
$this->allowed_extensions = array_change_key_case($this->allowed_extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an image file name from a Prophecy Image Service URI.
|
||||
*
|
||||
* @param string $src The fully qualified URI to the image.
|
||||
* @param string $extension The file extension/image type.
|
||||
* @param string $name An optional name for the file, without extension. Original src will be used otherwise.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function build_image_file_name(string $src, string $extension, string $name = ''): string {
|
||||
if (! $this->is_valid_image_extension($extension)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid file extension: %s', $extension));
|
||||
}
|
||||
|
||||
$filename = $name ?: pathinfo(rawurldecode($src), PATHINFO_FILENAME);
|
||||
|
||||
return sprintf('%s.%s', trim($filename), strtolower($extension));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the file name to save it as WordPress would.
|
||||
*
|
||||
* @param string $file_path The full server path to the saved image file.
|
||||
*
|
||||
* @throws ImageDownloadException
|
||||
*/
|
||||
public function format_file_path_for_wordpress(string $file_path, ResponseAdapter $response): string {
|
||||
$info = pathinfo($file_path);
|
||||
$dirname = $info['dirname'] ?? '';
|
||||
$ext = $info['extension'] ?? '';
|
||||
|
||||
if (! $dirname || ! $ext) {
|
||||
throw new ImageDownloadException('Missing dirname or extension');
|
||||
}
|
||||
|
||||
// WordPress scaled images do not have width/height in their names.
|
||||
if ($response->size === self::SCALED_SIZE) {
|
||||
return str_replace(".$ext", sprintf('-%s.%s', self::SCALED_SIZE, $ext), "$dirname/$response->filename");
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
[ $width, $height ] = getimagesize($file_path);
|
||||
|
||||
return str_replace(".$ext", "-{$width}x$height.$ext", "$dirname/$response->filename");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file extension is in the allowed types.
|
||||
*/
|
||||
private function is_valid_image_extension(string $extension): bool {
|
||||
return $this->allowed_extensions[strtolower($extension)] ?? false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Exceptions\ImageDownloadException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\DownloadedImage;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\ResponseAdapter;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\FileNameSanitizer;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Filesystem;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\File\File;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
|
||||
final class ImageDownloader
|
||||
{
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private HttpClientInterface $client;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private Filesystem $filesystem;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private LoggerInterface $logger;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private FileNameProcessor $file;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private FileNameSanitizer $sanitizer;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private int $batch_size;
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private bool $seo_names = true;
|
||||
/**
|
||||
* @var string[]
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private array $allowed_mime_types = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/png',
|
||||
];
|
||||
/**
|
||||
* @var string[]
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private array $allowed_hosts = [
|
||||
'images.pexels.com',
|
||||
];
|
||||
/**
|
||||
* A collection of successfully downloaded images.
|
||||
*
|
||||
* @var array<int, array<string, DownloadedImage>>
|
||||
*/
|
||||
private array $collected = [];
|
||||
|
||||
/**
|
||||
* A collection of nested exceptions.
|
||||
*
|
||||
* @var ImageDownloadException[]
|
||||
*/
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* @param string[] $allowed_mime_types The image mime types we allow to download.
|
||||
* @param string[] $allowed_hosts The host names that we are allowed to download images from.
|
||||
*/
|
||||
public function __construct(HttpClientInterface $client, Filesystem $filesystem, LoggerInterface $logger, FileNameProcessor $file, FileNameSanitizer $sanitizer, int $batch_size, bool $seo_names = true, array $allowed_mime_types = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/png',
|
||||
], array $allowed_hosts = [
|
||||
'images.pexels.com',
|
||||
]) {
|
||||
$this->client = $client;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->logger = $logger;
|
||||
$this->file = $file;
|
||||
$this->sanitizer = $sanitizer;
|
||||
$this->batch_size = $batch_size;
|
||||
$this->seo_names = $seo_names;
|
||||
$this->allowed_mime_types = $allowed_mime_types;
|
||||
$this->allowed_hosts = $allowed_hosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a collection of images to a path on the server.
|
||||
*
|
||||
* @param array<array{
|
||||
* collection_slug: string,
|
||||
* image_type: string,
|
||||
* images: array<int, array{
|
||||
* id: int,
|
||||
* width: int,
|
||||
* height: int,
|
||||
* alt: string,
|
||||
* url: string,
|
||||
* photographer: string,
|
||||
* photographer_url: string,
|
||||
* avg_color: string,
|
||||
* sizes: non-empty-array<int,array{name: string, src: string}>
|
||||
* }>
|
||||
* }> $image_response The response from our GraphQL images Query.
|
||||
* @param string $path The directory to save images in.
|
||||
*
|
||||
* @throws ImageDownloadException|\Throwable
|
||||
*
|
||||
* @return array<int, array<string, DownloadedImage>> A list of arrays, indexed by their thumbnail size name.
|
||||
*/
|
||||
public function download(array $image_response, string $path = '/tmp/prophecy-images'): array {
|
||||
$responses = [];
|
||||
$batch = $total = 0;
|
||||
|
||||
$this->logger->notice(sprintf('Building requests in batches of %d...', $this->batch_size));
|
||||
|
||||
foreach ($image_response as $collection) {
|
||||
foreach ($collection['images'] as $key => $image) {
|
||||
foreach ($image['sizes'] as $size) {
|
||||
// Use the alt tag as an SEO friendly filename. Prefix with the Pexels ID to ensure it remains unique.
|
||||
$name = $this->seo_names ? $image['alt'] . '-' . $image['id'] : '';
|
||||
$filename = $this->sanitizer->sanitize($this->file->build_image_file_name($size['src'], $collection['image_type'], $name));
|
||||
|
||||
try {
|
||||
if ($this->allowed_hosts) {
|
||||
$host = parse_url($size['src'], PHP_URL_HOST);
|
||||
|
||||
if (! $host || ! in_array($host, $this->allowed_hosts, true)) {
|
||||
throw new ImageDownloadException(
|
||||
sprintf('Unable to download image, host "%s" is not on allowed list.', $host)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$responses[] = new ResponseAdapter(
|
||||
$image['id'],
|
||||
$image['width'],
|
||||
$image['height'],
|
||||
$filename,
|
||||
$size['name'],
|
||||
$key,
|
||||
$image['alt'],
|
||||
$image['url'],
|
||||
$image['photographer'],
|
||||
$image['photographer_url'],
|
||||
$this->client->request('GET', $size['src'])
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->errors[] = new ImageDownloadException(sprintf('The response contains an error for image: %s', $size['src']), 1, $e);
|
||||
$this->logger->error($e->getMessage(), $e->getTrace());
|
||||
}
|
||||
|
||||
$batch++;
|
||||
|
||||
if ($batch >= $this->batch_size) {
|
||||
$this->logger->info(sprintf('Batch sized reached. Downloading %d images...', count($responses)));
|
||||
$this->processResponses($responses, $path);
|
||||
$total += $this->batch_size;
|
||||
$this->logger->info(sprintf('Total processed: %d', $total));
|
||||
|
||||
$responses = [];
|
||||
$batch = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($responses)) {
|
||||
$this->logger->info(sprintf('Processing remaining responses: %d', count($responses)));
|
||||
$this->processResponses($responses, $path);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->errors) {
|
||||
throw new ImageDownloadException('Some errors were detected when downloading images.', 1, array_pop($this->errors));
|
||||
}
|
||||
|
||||
return $this->collected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of responses.
|
||||
*
|
||||
* @param ResponseAdapter[] $responses
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function processResponses(array $responses, string $path): void {
|
||||
foreach ($responses as $response) {
|
||||
$ext = pathinfo($response->filename, PATHINFO_EXTENSION);
|
||||
$file_location = $path . '/' . md5($response->filename . $response->size . $response->id) . ".$ext";
|
||||
|
||||
try {
|
||||
$data = $response->response->getContent();
|
||||
|
||||
// Save the file, so we can use getimagesize() to read the dimensions.
|
||||
$this->filesystem->dumpFile($file_location, $data);
|
||||
|
||||
$file = new File($file_location);
|
||||
$mime = $file->getMimeType();
|
||||
|
||||
if (! $mime || ! in_array($mime, $this->allowed_mime_types, true)) {
|
||||
throw new ImageDownloadException(sprintf('Invalid mime type: %s', $mime));
|
||||
}
|
||||
|
||||
$save_path = $this->file->format_file_path_for_wordpress($file_location, $response);
|
||||
|
||||
// Multiple thumbnail sizes can end up with the exact same file name.
|
||||
if ($this->filesystem->exists($save_path)) {
|
||||
$this->logger->warning(sprintf('Image already exists: %s', $save_path));
|
||||
$this->filesystem->remove($file_location);
|
||||
} else {
|
||||
// Rename the file to the same format as WordPress.
|
||||
$this->filesystem->rename($file_location, $save_path);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->errors[] = new ImageDownloadException(sprintf('Failed to save image: %s', $file_location), 1, $e);
|
||||
$this->logger->error($e->getMessage(), $e->getTrace());
|
||||
$this->filesystem->remove($file_location);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->collected[$response->key][$response->size] = new DownloadedImage(
|
||||
$response->id,
|
||||
$response->width,
|
||||
$response->height,
|
||||
$save_path,
|
||||
$response->size,
|
||||
$response->alt,
|
||||
$response->url,
|
||||
$response->photographer,
|
||||
$response->photographer_url,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts\Sanitizer;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Sanitizers\DefaultFileNameSanitizer;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttpClient;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class ImageProvider extends Provider
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function register(): void {
|
||||
$this->container->bind(SluggerInterface::class, AsciiSlugger::class);
|
||||
$this->container->bind(Sanitizer::class, DefaultFileNameSanitizer::class);
|
||||
|
||||
$this->container->when(FileNameProcessor::class)
|
||||
->needs('$allowed_extensions')
|
||||
->give([
|
||||
'jpg' => true,
|
||||
'jpeg' => true,
|
||||
'webp' => true,
|
||||
'png' => true,
|
||||
]);
|
||||
|
||||
$this->container->when(ImageDownloader::class)
|
||||
->needs(HttpClientInterface::class)
|
||||
->give(HttpClient::create());
|
||||
|
||||
$this->container->when(ImageDownloader::class)
|
||||
->needs('$batch_size')
|
||||
->give((int) $this->config->get('image_batch_size'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models;
|
||||
|
||||
/**
|
||||
* Represents the results of a downloaded image.
|
||||
*/
|
||||
final class DownloadedImage
|
||||
{
|
||||
|
||||
public int $id;
|
||||
|
||||
public int $width;
|
||||
|
||||
public int $height;
|
||||
|
||||
public string $file;
|
||||
|
||||
public string $size;
|
||||
|
||||
public string $alt;
|
||||
|
||||
public string $url;
|
||||
|
||||
public string $photographer;
|
||||
|
||||
public string $photographer_url;
|
||||
/**
|
||||
* @param int $id The unique Pexels ID.
|
||||
* @param int $width The image's original max width.
|
||||
* @param int $height The image's original max height.
|
||||
* @param string $file The server path to the file.
|
||||
* @param string $size The WordPress size name, e.g. thumbnail.
|
||||
* @param string $alt The alt description for the image.
|
||||
* @param string $url The Pexels attachment URL.
|
||||
* @param string $photographer The photographer's name.
|
||||
* @param string $photographer_url The photographer's Pexels URL.
|
||||
*/
|
||||
public function __construct(int $id, int $width, int $height, string $file, string $size, string $alt, string $url, string $photographer, string $photographer_url) {
|
||||
$this->id = $id;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->file = $file;
|
||||
$this->size = $size;
|
||||
$this->alt = $alt;
|
||||
$this->url = $url;
|
||||
$this->photographer = $photographer;
|
||||
$this->photographer_url = $photographer_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, width: int, height: int, file: string, size: string, alt: string, url: string, photographer: string, photographer_url: string}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'file' => $this->file,
|
||||
'size' => $this->size,
|
||||
'alt' => $this->alt,
|
||||
'url' => $this->url,
|
||||
'photographer' => $this->photographer,
|
||||
'photographer_url' => $this->photographer_url,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
final class ResponseAdapter
|
||||
{
|
||||
|
||||
public int $id;
|
||||
|
||||
public int $width;
|
||||
|
||||
public int $height;
|
||||
|
||||
public string $filename;
|
||||
|
||||
public string $size;
|
||||
|
||||
public int $key;
|
||||
|
||||
public string $alt;
|
||||
|
||||
public string $url;
|
||||
|
||||
public string $photographer;
|
||||
|
||||
public string $photographer_url;
|
||||
public ResponseInterface $response;
|
||||
/**
|
||||
* @param int $id The unique Pexels ID.
|
||||
* @param int $width The image's original max width.
|
||||
* @param int $height The image's original max height.
|
||||
* @param string $filename The file name with extension.
|
||||
* @param string $size The WordPress size name, e.g. thumbnail.
|
||||
* @param int $key The index associated with the collection as we loop through it.
|
||||
* @param string $alt The alt description for the image.
|
||||
* @param string $url The Pexels attachment URL.
|
||||
* @param string $photographer The photographer's name.
|
||||
* @param string $photographer_url The photographer's Pexels URL.
|
||||
*/
|
||||
public function __construct(int $id, int $width, int $height, string $filename, string $size, int $key, string $alt, string $url, string $photographer, string $photographer_url, ResponseInterface $response) {
|
||||
$this->id = $id;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->filename = $filename;
|
||||
$this->size = $size;
|
||||
$this->key = $key;
|
||||
$this->alt = $alt;
|
||||
$this->url = $url;
|
||||
$this->photographer = $photographer;
|
||||
$this->photographer_url = $photographer_url;
|
||||
$this->response = $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# Prophecy Image Downloader
|
||||
|
||||
> ⚠️ **This is a read-only repository!**
|
||||
> For pull requests or issues, see [stellarwp/prophecy-monorepo](https://github.com/stellarwp/prophecy-monorepo).
|
||||
|
||||
A very fast, asynchronous concurrent image downloader built on [symfony/http-client](https://symfony.com/doc/current/http_client.html).
|
||||
|
||||
## Installation
|
||||
|
||||
Update your composer.json and add the following to your `repositories` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "git@github.com:stellarwp/prophecy-container.git"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "git@github.com:stellarwp/prophecy-log.git"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "git@github.com:stellarwp/prophecy-image-downloader.git"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then, install:
|
||||
|
||||
```shell
|
||||
composer require stellarwp/prophecy-image-downloader
|
||||
```
|
||||
|
||||
If using the [di52 container](https://github.com/lucatume/di52), create a `config.php` and register
|
||||
it in the container with:
|
||||
|
||||
```php
|
||||
$this->container->bind(Dot::class, new Dot(require_once dirname(__FILE__) . '/config.php'));
|
||||
```
|
||||
|
||||
The config.php file map environment variables, either from an `.env` file or manually set, e.g.
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'image_batch_size' => $_ENV['APP_IMAGE_BATCH_SIZE'] ?? 10,
|
||||
'storage_path' => $_ENV['APP_STORAGE_PATH'] ?? '/tmp', // This could be the path to your wp-content/uploads folder.
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
Then, include the [ImageProvider.php](./ImageProvider.php), or create a custom/modified version
|
||||
of it and call the `register` method.
|
||||
|
||||
The downloader is meant to work with the [Prophecy Laravel Service](https://github.com/stellarwp/prophecy)'s API responses, however
|
||||
if you format your array in the same way that [ImageDownloader::download()](./ImageDownloader.php) accepts, it will
|
||||
fetch the files to your configured location:
|
||||
|
||||
|
||||
## File Name Sanitization
|
||||
|
||||
The system provides some basic file name sanitization, but you can also use your framework's strategy or
|
||||
make a custom strategy, as long as it implements the [Sanitizer Contract](./Sanitization/Contracts/Sanitizer.php).
|
||||
|
||||
For example, to use WordPress's file name sanitization, simply bind the interface to the provided WPFileNameStrategy, e.g. using
|
||||
di52's container:
|
||||
|
||||
```php
|
||||
$this->container->bind(Sanitizer::class, \StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Sanitizers\WPFileNameSanitizer::class);
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts;
|
||||
|
||||
interface Sanitizer
|
||||
{
|
||||
/**
|
||||
* Sanitize a file name.
|
||||
*/
|
||||
public function __invoke(string $filename): string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts\Sanitizer;
|
||||
|
||||
/**
|
||||
* Sanitizes a filename using a callable/Closure strategy.
|
||||
*/
|
||||
final class FileNameSanitizer
|
||||
{
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private Sanitizer $sanitizer;
|
||||
/**
|
||||
* @param Sanitizer $sanitizer The filename sanitizer Closure strategy.
|
||||
*/
|
||||
public function __construct(Sanitizer $sanitizer) {
|
||||
$this->sanitizer = $sanitizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a filename.
|
||||
*/
|
||||
public function sanitize(string $filename): string {
|
||||
return ($this->sanitizer)($filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Sanitizers;
|
||||
|
||||
use RuntimeException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts\Sanitizer;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
/**
|
||||
* The default filename sanitizer.
|
||||
*/
|
||||
final class DefaultFileNameSanitizer implements Sanitizer
|
||||
{
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private SluggerInterface $slugger;
|
||||
public function __construct(SluggerInterface $slugger) {
|
||||
$this->slugger = $slugger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __invoke(string $filename): string {
|
||||
// Dot files are renamed regardless of security settings.
|
||||
$filename = trim($filename, '.');
|
||||
|
||||
// Remove any null bytes. See
|
||||
// http://php.net/manual/security.filesystem.nullbytes.php
|
||||
$filename = str_replace(chr(0), '', $filename);
|
||||
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
if (empty($ext)) {
|
||||
throw new RuntimeException('Cannot sanitize dot files');
|
||||
}
|
||||
|
||||
$original = pathinfo($filename, PATHINFO_FILENAME);
|
||||
|
||||
return $this->slugger->slug($original)->lower() . ".$ext";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Sanitizers;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts\Sanitizer;
|
||||
|
||||
/**
|
||||
* The WordPress filename sanitizer.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class WPFileNameSanitizer implements Sanitizer
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __invoke(string $filename): string {
|
||||
// @phpstan-ignore-next-line
|
||||
return strtolower(sanitize_file_name($filename));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user