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,212 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Hasher;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\Shutdown\Contracts\Terminable;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Prime Pexels HTTP cache for future image downloading.
|
||||
*/
|
||||
final class Cache_Primer implements Terminable {
|
||||
|
||||
/**
|
||||
* @var HttpClientInterface
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var Hasher
|
||||
*/
|
||||
private $hasher;
|
||||
|
||||
/**
|
||||
* How long in seconds to wait until we remotely prime the collection of images again.
|
||||
*
|
||||
* @var int Time in seconds.
|
||||
*/
|
||||
private $cache_duration;
|
||||
|
||||
/**
|
||||
* How many external cache requests to create before removing them.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $batch_size;
|
||||
|
||||
/**
|
||||
* The collections cache, to run when the class is destroyed.
|
||||
*
|
||||
* @var 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}>
|
||||
* }>
|
||||
* }> $collections
|
||||
*/
|
||||
private $collections;
|
||||
|
||||
/**
|
||||
* @param HttpClientInterface $client The HTTP client.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param Hasher $hasher The hasher.
|
||||
* @param int $cache_duration The cache duration in seconds.
|
||||
* @param int $batch_size How many external cache requests to create before removing them.
|
||||
*/
|
||||
public function __construct(
|
||||
HttpClientInterface $client,
|
||||
LoggerInterface $logger,
|
||||
Hasher $hasher,
|
||||
int $cache_duration,
|
||||
int $batch_size = 500
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->logger = $logger;
|
||||
$this->hasher = $hasher;
|
||||
$this->cache_duration = $cache_duration;
|
||||
$this->batch_size = $batch_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign which collections will be primed on the WordPress shutdown hook.
|
||||
*
|
||||
* @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}>
|
||||
* }>
|
||||
* }> $collections
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init( array $collections ): void {
|
||||
if ( empty( $collections ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->collections = $collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime cache on shutdown.
|
||||
*
|
||||
* @action shutdown
|
||||
*
|
||||
* @return void
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function terminate(): void {
|
||||
$this->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* On shutdown, make asynchronous HEAD requests to all the potential images we'll download
|
||||
* to ensure that Pexels caches their response to make downloading much quicker.
|
||||
*
|
||||
* This allows for 0 blocking.
|
||||
*
|
||||
* @action shutdown
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function execute(): void {
|
||||
if ( ! isset( $this->collections ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch = 0;
|
||||
$cache_key = $this->hasher->hash( $this->collections );
|
||||
|
||||
if ( get_transient( $cache_key ) !== false ) {
|
||||
$this->logger->debug( sprintf( 'Found cache key "%s", skipping image cache priming', $cache_key ) );
|
||||
|
||||
unset( $this->collections );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Search results differ slightly from industry collections, reformat.
|
||||
if ( isset( $this->collections['images'] ) ) {
|
||||
$this->collections = [ $this->collections ];
|
||||
}
|
||||
|
||||
foreach ( $this->collections as $collection ) {
|
||||
|
||||
$this->logger->debug( sprintf( 'Priming image cache for %d images...', count( $collection['images'] ) ) );
|
||||
|
||||
foreach ( $collection['images'] as $image ) {
|
||||
|
||||
$this->logger->debug( sprintf( 'Priming image cache for: %s', $image['url'] ) );
|
||||
|
||||
foreach ( $image['sizes'] as $size ) {
|
||||
try {
|
||||
$batch++;
|
||||
|
||||
$url = $size['src'];
|
||||
|
||||
// These are async requests; We won't wait for the responses.
|
||||
$promises[ $url ] = $this->client->request( 'HEAD', $url, [
|
||||
'timeout' => 0.1,
|
||||
'max_duration' => 0.1,
|
||||
] );
|
||||
} catch ( Throwable $e ) {
|
||||
}
|
||||
|
||||
// Remove existing promises when batch size reached.
|
||||
if ( $batch >= $this->batch_size ) {
|
||||
$batch = 0;
|
||||
|
||||
try {
|
||||
unset( $promises );
|
||||
} catch ( Throwable $e ) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any remaining promises.
|
||||
try {
|
||||
unset( $promises );
|
||||
} catch ( Throwable $e ) {
|
||||
}
|
||||
|
||||
// Clear collections state in case this is accessed again in the same request.
|
||||
unset( $this->collections );
|
||||
|
||||
$this->logger->debug( sprintf( 'Caching image priming using key "%s" for %d seconds', $cache_key, $this->cache_duration ) );
|
||||
|
||||
set_transient( $cache_key, true, $this->cache_duration );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Exceptions\ImageDownloadException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\ImageDownloader;
|
||||
use Throwable;
|
||||
|
||||
final class Image_Downloader {
|
||||
|
||||
/**
|
||||
* @var ImageDownloader
|
||||
*/
|
||||
private $downloader;
|
||||
|
||||
/**
|
||||
* @var WordPress_Importer
|
||||
*/
|
||||
private $importer;
|
||||
|
||||
/**
|
||||
* @var Pexels_ID_Registry
|
||||
*/
|
||||
private $registry;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
public function __construct(
|
||||
ImageDownloader $downloader,
|
||||
WordPress_Importer $importer,
|
||||
Pexels_ID_Registry $registry,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->downloader = $downloader;
|
||||
$this->importer = $importer;
|
||||
$this->registry = $registry;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and import a collection of images to the WordPress library.
|
||||
*
|
||||
* @param array<array{
|
||||
* collection_slug?: string,
|
||||
* image_type?: string,
|
||||
* images: array<int, array{
|
||||
* id: int,
|
||||
* post_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}>
|
||||
* }>
|
||||
* }> $images
|
||||
*
|
||||
* @return array<array{id: int, url: string}>
|
||||
* @throws ImageDownloadException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download( array $images ): array {
|
||||
$existing = $this->get_existing_images( $images );
|
||||
$downloaded = empty( $images['images'] ) ? [] : $this->download_images( $images );
|
||||
|
||||
// Merge any imported images with existing images.
|
||||
return array_merge( $existing, $downloaded );
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the incoming request, determine which images have already been
|
||||
* downloaded.
|
||||
*
|
||||
* This is weird because the frontend replaces the id and the URL, which is normally
|
||||
* the pexels_id and pexels URL with the attachment_id and full URL to the image.
|
||||
*
|
||||
* Now local images will pass a post_id, but depending on the image state, we still
|
||||
* have to check the meta for the pexels_id, because the image may exist in the media
|
||||
* library.
|
||||
*
|
||||
* @param array<array{
|
||||
* collection_slug?: string,
|
||||
* image_type?: string,
|
||||
* images: array<int, array{
|
||||
* id: int,
|
||||
* post_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}>
|
||||
* }>
|
||||
* }> $images
|
||||
*
|
||||
* @return array<array{id: int, url: string}>
|
||||
*/
|
||||
private function get_existing_images( array &$images ): array {
|
||||
$existing = [];
|
||||
$ids = $this->registry->all();
|
||||
|
||||
foreach ( $images['images'] as $key => $image ) {
|
||||
$post_id = $image['post_id'] ?? false;
|
||||
$pexels_id = $image['id'] ?? false;
|
||||
|
||||
if ( $post_id !== false ) {
|
||||
// We were provided a post_id, but it's not in our list of attachments.
|
||||
if ( ! isset( $ids['post_ids'][ $post_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
} elseif ( $pexels_id !== false ) {
|
||||
$post_id = $ids['pexels_ids'][ $pexels_id ] ?? false;
|
||||
|
||||
if ( $post_id === false ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$existing[] = [
|
||||
'id' => $post_id,
|
||||
'url' => wp_get_attachment_image_url( $post_id ),
|
||||
];
|
||||
|
||||
unset( $images['images'][ $key ] );
|
||||
}
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and import Pexels images to the WordPress library.
|
||||
*
|
||||
* @param array<array{
|
||||
* collection_slug?: string,
|
||||
* image_type?: string,
|
||||
* images: array<int, array{
|
||||
* id: int,
|
||||
* post_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}>
|
||||
* }>
|
||||
* }> $images
|
||||
*
|
||||
* @return array<array{id: int, url: string}>
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function download_images( array $images ): array {
|
||||
if ( ! current_user_can( 'upload_files' ) ) {
|
||||
$this->logger->warning( 'User doesn\'t have permission to upload files. Aborting image download' );
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = wp_get_upload_dir()['path'] ?? '';
|
||||
|
||||
if ( ! $path ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! isset( $images['image_type'] ) ) {
|
||||
$images['image_type'] = 'jpg';
|
||||
}
|
||||
|
||||
if ( ! isset( $images['collection_slug'] ) ) {
|
||||
$images['collection_slug'] = wp_generate_password( 12, false );
|
||||
}
|
||||
|
||||
$collection[] = $images;
|
||||
|
||||
try {
|
||||
$downloaded = $this->downloader->download( $collection, $path );
|
||||
|
||||
return $this->importer->import( $downloaded );
|
||||
} catch ( Throwable $e ) {
|
||||
$this->logger->error( 'Image download or import error', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
] );
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Hasher;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\FileNameProcessor;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\ImageDownloader;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Contracts\Sanitizer;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Sanitization\Sanitizers\WPFileNameSanitizer;
|
||||
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 Image_Downloader_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function register(): void {
|
||||
// Create the HTTP Client used to concurrently download images.
|
||||
$this->container->bind( HttpClientInterface::class, HttpClient::create() );
|
||||
|
||||
$this->register_hasher();
|
||||
$this->register_cache_primer();
|
||||
$this->register_image_downloader();
|
||||
}
|
||||
|
||||
private function register_hasher(): void {
|
||||
$this->container->when( Hasher::class )
|
||||
->needs( '$algo' )
|
||||
->give(
|
||||
static function (): string {
|
||||
return PHP_VERSION_ID >= 80100 ? 'xxh128' : 'md5';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function register_cache_primer(): void {
|
||||
/**
|
||||
* Filter how many external cache requests we will open at once before discarding them.
|
||||
*
|
||||
* This may need to be adjusted depending on the host's limitations.
|
||||
*
|
||||
* @param int $batch_size The number of external cache requests per batch.
|
||||
*/
|
||||
$batch_size = absint( apply_filters( 'kadence_blocks_cache_primer_batch_size', 500 ) );
|
||||
|
||||
/**
|
||||
* How long in seconds to wait until we remotely prime the collection of images again.
|
||||
*
|
||||
* @param int $cache_duration Time in seconds.
|
||||
*/
|
||||
$cache_duration = absint( apply_filters( 'kadence_blocks_cache_primer_cache_duration', HOUR_IN_SECONDS ) );
|
||||
|
||||
$this->container->when( Cache_Primer::class )
|
||||
->needs( '$batch_size' )
|
||||
->give( $batch_size );
|
||||
|
||||
$this->container->when( Cache_Primer::class )
|
||||
->needs( '$cache_duration' )
|
||||
->give( $cache_duration );
|
||||
|
||||
$this->container->singleton( Cache_Primer::class, Cache_Primer::class );
|
||||
}
|
||||
|
||||
private function register_image_downloader(): void {
|
||||
$this->container->bind( SluggerInterface::class, AsciiSlugger::class );
|
||||
$this->container->bind( Sanitizer::class, WPFileNameSanitizer::class );
|
||||
|
||||
// Ensure we always get the same instance, so the image state is current.
|
||||
$this->container->singleton( WordPress_Importer::class, WordPress_Importer::class );
|
||||
|
||||
// Configure the allowed file extensions that are allowed to be processed.
|
||||
$this->container->when( FileNameProcessor::class )
|
||||
->needs( '$allowed_extensions' )
|
||||
->give(
|
||||
[
|
||||
'jpg' => true,
|
||||
'jpeg' => true,
|
||||
'webp' => true,
|
||||
'png' => true,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter how many concurrent download requests we will open at once before we attempt to save
|
||||
* the images to disk.
|
||||
*
|
||||
* This may need to be adjusted depending on the host's limitations.
|
||||
*
|
||||
* @param int $batch_size The number of download requests per batch.
|
||||
*/
|
||||
$batch_size = absint( apply_filters( 'kadence_blocks_image_download_batch_size', 200 ) );
|
||||
|
||||
$this->container->when( ImageDownloader::class )
|
||||
->needs( '$batch_size' )
|
||||
->give( $batch_size );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\FileNameProcessor;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\DownloadedImage;
|
||||
use KadenceWP\KadenceBlocks\Traits\Image_Size_Trait;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
use WP_Image_Editor;
|
||||
|
||||
/**
|
||||
* Normally the WP_Image_Editor will create all the different thumbnail sizes on the server,
|
||||
* however, we simply return the data of the images that have already been downloaded with
|
||||
* the concurrent image downloader.
|
||||
*/
|
||||
final class Image_Editor extends WP_Image_Editor {
|
||||
|
||||
use Image_Size_Trait;
|
||||
|
||||
/**
|
||||
* The collection of all downloaded images.
|
||||
*
|
||||
* @var array<int, DownloadedImage[]>
|
||||
*/
|
||||
private $images = [];
|
||||
|
||||
/**
|
||||
* The current DownloadedImage being processed.
|
||||
*
|
||||
* @var DownloadedImage
|
||||
*/
|
||||
private $image;
|
||||
|
||||
/**
|
||||
* The current Pexels ID being processed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Tests if this Image Editor is supported.
|
||||
*
|
||||
* @param array $args
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function test( $args = [] ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We support all mime types because this editor is only loaded
|
||||
* when the Image Downloader is executed.
|
||||
*
|
||||
* @param string $mime_type
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public static function supports_mime_type( $mime_type ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is always called first, so basically an init method.
|
||||
*
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function load() {
|
||||
if ( $this->file === null ) {
|
||||
return new WP_Error( 'error_loading_image', __( 'File cannot be null.', 'kadence-blocks' ) );
|
||||
}
|
||||
|
||||
$this->logger = kadence_blocks()->get( LoggerInterface::class );
|
||||
|
||||
// Fetch the currently downloaded images from the Importer.
|
||||
$this->images = kadence_blocks()->get( WordPress_Importer::class )->images();
|
||||
|
||||
// Find the image WordPress is currently processing by matching the file name.
|
||||
foreach ( $this->images as $id => $images ) {
|
||||
// Grab the scaled image, or fallback to the largest size.
|
||||
$scaled_key = array_search( FileNameProcessor::SCALED_SIZE, array_column( $images, 'size' ), true );
|
||||
|
||||
if ( $scaled_key !== false ) {
|
||||
$scaled = $images[ $scaled_key ] ?? end( $images );
|
||||
} else {
|
||||
$scaled = end( $images );
|
||||
}
|
||||
|
||||
if ( $this->file !== $scaled->file ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->image = $scaled;
|
||||
$this->id = $id;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $this->image === null ) {
|
||||
$this->logger->error( 'Cannot find downloaded file', [
|
||||
'file' => $this->file,
|
||||
] );
|
||||
|
||||
return new WP_Error( 'error_loading_image', __( 'Cannot find downloaded file.', 'kadence-blocks' ), $this->file );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @return array{path: string, file: string, width: int, height: int, mime-type: string, filesize: int}
|
||||
*/
|
||||
public function save( $destfilename = null, $mime_type = null ): array {
|
||||
$image_size = wp_getimagesize( $this->image->file );
|
||||
|
||||
return [
|
||||
'path' => $this->image->file,
|
||||
'file' => wp_basename( $this->image->file ),
|
||||
'width' => $image_size[0],
|
||||
'height' => $image_size[1],
|
||||
'mime-type' => $image_size['mime'],
|
||||
'filesize' => wp_filesize( $this->image->file ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Our images are already resized, just return true.
|
||||
*
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resize( $max_w, $max_h, $crop = false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple smaller images from a single source.
|
||||
*
|
||||
* Attempts to create all sub-sizes and returns the meta data at the end.
|
||||
*
|
||||
* As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
|
||||
* the new images one at a time and allows for the meta data to be saved after
|
||||
* each new image is created.
|
||||
*
|
||||
* @param array<string, array{width?: int, height?: int, crop?: bool}> $sizes
|
||||
*
|
||||
* @return array<string,array{path: string, file: string, width: int, height: int, mime-type: string, filesize: int}> An array of resized images' metadata by size.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function multi_resize( $sizes ) {
|
||||
$this->logger->debug( 'Using old multi_resize method' );
|
||||
|
||||
$metadata = [];
|
||||
|
||||
foreach ( $sizes as $size => $size_data ) {
|
||||
$meta = $this->make_subsize( $size_data );
|
||||
|
||||
if ( ! is_wp_error( $meta ) ) {
|
||||
$metadata[ $size ] = $meta;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger->error( 'Unable to make image subsize', [
|
||||
'file' => $this->image->file,
|
||||
'errors' => $meta->get_error_messages(),
|
||||
] );
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Our images are already cropped, just return true.
|
||||
*
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rotate( $angle ) {
|
||||
throw new RuntimeException( 'method not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function flip( $horz, $vert ) {
|
||||
throw new RuntimeException( 'method not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function stream( $mime_type = null ) {
|
||||
throw new RuntimeException( 'method not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find our already made sub-sized images in our image collection.
|
||||
*
|
||||
* @param array{width?: int, height?: int, crop?: bool} $size_data
|
||||
*
|
||||
* @return WP_Error|array{path: string, file: string, width: int, height: int, mime-type: string, filesize: int}
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function make_subsize( $size_data ) {
|
||||
if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
|
||||
$this->logger->error( 'Cannot resize the image. Both width and height are not set.', [
|
||||
'file' => $this->image->file,
|
||||
] );
|
||||
|
||||
return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $size_data['width'] ) ) {
|
||||
$size_data['width'] = null;
|
||||
}
|
||||
|
||||
if ( ! isset( $size_data['height'] ) ) {
|
||||
$size_data['height'] = null;
|
||||
}
|
||||
|
||||
if ( ! isset( $size_data['crop'] ) ) {
|
||||
$size_data['crop'] = false;
|
||||
}
|
||||
|
||||
$existing_sizes = $this->get_image_sizes();
|
||||
$thumbnail_id = '';
|
||||
|
||||
// Find the thumbnail name based on the requested dimensions.
|
||||
foreach ( $existing_sizes as $existing_size ) {
|
||||
if ( $existing_size['width'] === $size_data['width'] && $existing_size['height'] === $size_data['height'] ) {
|
||||
$thumbnail_id = $existing_size['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( strlen( $thumbnail_id ) === 0 ) {
|
||||
$this->logger->error( 'Could not find thumbnail size', [
|
||||
'file' => $this->image->file,
|
||||
'requested_width' => $size_data['width'],
|
||||
'requested_height' => $size_data['height'],
|
||||
] );
|
||||
|
||||
return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image.', 'kadence-blocks' ) );
|
||||
}
|
||||
|
||||
// Find the matching file for the requested thumbnail size and get its metadata.
|
||||
foreach ( $this->images[ $this->id ] as $key => $image ) {
|
||||
if ( $image->size !== $thumbnail_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$original = $this->image;
|
||||
$this->image = $image;
|
||||
|
||||
$saved = $this->save();
|
||||
|
||||
$this->image = $original;
|
||||
|
||||
unset( $this->images[ $this->id ][ $key ] );
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
$this->logger->error( 'Cannot match image to size data', [
|
||||
'file' => $this->image->file,
|
||||
'file_max_width' => $this->image->width,
|
||||
'file_max_height' => $this->image->height,
|
||||
'requested_width' => $size_data['width'],
|
||||
'requested_height' => $size_data['height'],
|
||||
] );
|
||||
|
||||
return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image.', 'kadence-blocks' ) );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\DownloadedImage;
|
||||
|
||||
/**
|
||||
* Manages Kadence/Pexels image meta.
|
||||
*/
|
||||
final class Meta {
|
||||
|
||||
public const ATTACHMENT_ALT = '_wp_attachment_image_alt';
|
||||
public const PEXELS_PHOTOGRAPHER = '_pexels_photographer';
|
||||
public const PEXELS_PHOTOGRAPHER_URL = '_pexels_photographer_url';
|
||||
public const PEXELS_ID = '_pexels_id';
|
||||
|
||||
public const DELETABLE = [
|
||||
self::PEXELS_PHOTOGRAPHER,
|
||||
self::PEXELS_PHOTOGRAPHER_URL,
|
||||
self::PEXELS_ID,
|
||||
];
|
||||
|
||||
/**
|
||||
* Insert additional image metadata when downloaded images are added.
|
||||
*
|
||||
* @param int $attachment_id
|
||||
* @param DownloadedImage $image
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add( int $attachment_id, DownloadedImage $image ): void {
|
||||
$meta = [
|
||||
self::ATTACHMENT_ALT => $image->alt,
|
||||
self::PEXELS_PHOTOGRAPHER => $image->photographer,
|
||||
self::PEXELS_PHOTOGRAPHER_URL => $image->photographer_url,
|
||||
self::PEXELS_ID => $image->id,
|
||||
];
|
||||
|
||||
foreach ( $meta as $meta_key => $value ) {
|
||||
if ( strlen( (string) $value ) <= 0 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
update_post_meta( $attachment_id, $meta_key, wp_slash( sanitize_text_field( $value ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete image metadata.
|
||||
*
|
||||
* @param int $attachment_id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete( int $attachment_id ): void {
|
||||
foreach ( self::DELETABLE as $meta_key ) {
|
||||
delete_post_meta( $attachment_id, $meta_key );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
final class Pexels_ID_Registry {
|
||||
|
||||
/**
|
||||
* A reverse map of keyed post_id => pexels_id and pexels_id => post_id.
|
||||
*
|
||||
* @note A bit worried about memory issues here, but this is a more efficient
|
||||
* query than making thousands of meta queries.
|
||||
*
|
||||
* @var array{post_ids: array<int, int>, pexels_ids: array<int, int>}
|
||||
*/
|
||||
private $ids = [];
|
||||
|
||||
/**
|
||||
* Returns a reverse map of keyed post_id => pexels_id and pexels_id => post_id.
|
||||
*
|
||||
* @return array{post_ids: array<int, int>, pexels_ids: array<int, int>}
|
||||
*/
|
||||
public function all(): array {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! empty( array_filter( $this->ids ) ) ) {
|
||||
return $this->ids;
|
||||
}
|
||||
|
||||
/** @var array<int, array{post_id: string, pexels_id: string}> $hashes */
|
||||
$ids = $wpdb->get_results( $wpdb->prepare( "
|
||||
SELECT `post_id`, `meta_value` as pexels_id
|
||||
FROM $wpdb->postmeta
|
||||
WHERE `meta_key` = %s
|
||||
",
|
||||
Meta::PEXELS_ID
|
||||
), ARRAY_A );
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$this->ids['post_ids'][ (int) $id['post_id'] ] = (int) $id['pexels_id'];
|
||||
$this->ids['pexels_ids'][ (int) $id['pexels_id'] ] = (int) $id['post_id'];
|
||||
}
|
||||
|
||||
return $this->ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Image_Downloader;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\FileNameProcessor;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\ImageDownloader\Models\DownloadedImage;
|
||||
|
||||
final class WordPress_Importer {
|
||||
|
||||
/**
|
||||
* The image data, indexed by Pexels ID.
|
||||
*
|
||||
* @var array<int, DownloadedImage[]>
|
||||
*/
|
||||
private $images = [];
|
||||
|
||||
/**
|
||||
* Manages image metadata.
|
||||
*
|
||||
* @var Meta
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @param Meta $meta
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function __construct( Meta $meta, LoggerInterface $logger ) {
|
||||
$this->meta = $meta;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, DownloadedImage[]>
|
||||
*/
|
||||
public function images(): array {
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import already downloaded images into the WordPress media library.
|
||||
*
|
||||
* @param array<int, array<string, DownloadedImage>> $collections
|
||||
*
|
||||
* @return array<array{id: int, url: string}>
|
||||
*/
|
||||
public function import( array $collections ): array {
|
||||
$upload = wp_get_upload_dir();
|
||||
$stored = [];
|
||||
|
||||
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
|
||||
include( ABSPATH . 'wp-admin/includes/image.php' );
|
||||
}
|
||||
|
||||
// Combine thumbnail images under their Pexels ID.
|
||||
foreach ( $collections as $images ) {
|
||||
foreach ( $images as $image ) {
|
||||
$this->images[ $image->id ][] = $image;
|
||||
}
|
||||
}
|
||||
|
||||
// Override the WP image editor with our custom null editor.
|
||||
$existing_editors = [];
|
||||
add_filter( 'wp_image_editors', static function ( $editors ) use ( &$existing_editors ) {
|
||||
$existing_editors = $editors;
|
||||
|
||||
return [ '\\KadenceWP\\KadenceBlocks\\Image_Downloader\\Image_Editor' ];
|
||||
}, 8, 1 );
|
||||
|
||||
foreach ( $this->images as $id => $images ) {
|
||||
// Grab the scaled image, or fallback to the largest size.
|
||||
$scaled_key = array_search( FileNameProcessor::SCALED_SIZE, array_column( $images, 'size' ), true );
|
||||
|
||||
if ( $scaled_key !== false ) {
|
||||
$scaled = $this->images[ $id ][ $scaled_key ] ?? end( $this->images[ $id ] );
|
||||
} else {
|
||||
$scaled = end( $this->images[ $id ] );
|
||||
}
|
||||
|
||||
$info = wp_check_filetype( $scaled->file );
|
||||
// Translators: %s is the photographer's name.
|
||||
$title = sprintf( __( 'Photo by %s', 'kadence-blocks' ), $scaled->photographer );
|
||||
$filename = $this->get_file_name( $scaled );
|
||||
$uploaded_url = $upload['url'] . "/$filename";
|
||||
|
||||
$attachment = [
|
||||
'guid' => $uploaded_url,
|
||||
'post_mime_type' => $info['type'],
|
||||
'post_title' => $title,
|
||||
'post_content' => '',
|
||||
];
|
||||
|
||||
$attachment_id = wp_insert_attachment( $attachment, $scaled->file );
|
||||
|
||||
if ( $attachment_id <= 0 ) {
|
||||
$this->logger->error( 'Failed to insert attachment', [
|
||||
'file' => $scaled->file
|
||||
] );
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
wp_generate_attachment_metadata( $attachment_id, $scaled->file );
|
||||
|
||||
$this->meta->add( $attachment_id, $scaled );
|
||||
|
||||
$stored[] = [
|
||||
'id' => $attachment_id,
|
||||
'url' => $uploaded_url,
|
||||
];
|
||||
}
|
||||
|
||||
// Reset the original WP image editors.
|
||||
add_filter( 'wp_image_editors', static function () use ( $existing_editors ) {
|
||||
return $existing_editors;
|
||||
}, 9, 0 );
|
||||
|
||||
return $stored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file name and extension from a server path.
|
||||
*
|
||||
* @param DownloadedImage $image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_file_name( DownloadedImage $image ): string {
|
||||
return pathinfo( $image->file, PATHINFO_BASENAME );
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user