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,46 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Admin;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
|
||||
/**
|
||||
* Dashboard / wp-admin container definitions and hooks.
|
||||
*/
|
||||
final class Admin_Provider extends Provider {
|
||||
|
||||
public const HANDLE_POST_SAVED_EVENT = 'post-saved-event';
|
||||
|
||||
public function register(): void {
|
||||
add_action(
|
||||
'admin_init',
|
||||
function (): void {
|
||||
$this->register_post_saved_event();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post-saved-event action that fires when a Gutenberg post is saved.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_post_saved_event(): void {
|
||||
global $pagenow;
|
||||
|
||||
// Only load on post edit screens (not widgets.php or site-editor.php).
|
||||
if ( ! in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'enqueue_block_editor_assets',
|
||||
function (): void {
|
||||
$asset = $this->container->get( Asset::class );
|
||||
|
||||
$asset->enqueue_script( self::HANDLE_POST_SAVED_EVENT, 'dist/post-saved-event' );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
109
wp-content/plugins/kadence-blocks/includes/resources/App.php
Normal file
109
wp-content/plugins/kadence-blocks/includes/resources/App.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Adbar\Dot;
|
||||
use KadenceWP\KadenceBlocks\Admin\Admin_Provider;
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset_Provider;
|
||||
use KadenceWP\KadenceBlocks\Cache\Cache_Provider;
|
||||
use KadenceWP\KadenceBlocks\Database\Database_Provider;
|
||||
use KadenceWP\KadenceBlocks\Health\Health_Provider;
|
||||
use KadenceWP\KadenceBlocks\Image_Downloader\Image_Downloader_Provider;
|
||||
use KadenceWP\KadenceBlocks\Log\Log_Provider;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Optimizer_Provider;
|
||||
use KadenceWP\KadenceBlocks\Shutdown\Shutdown_Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ContainerContract\ContainerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Container;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Providable;
|
||||
use KadenceWP\KadenceBlocks\Harbor\Harbor_Provider;
|
||||
use KadenceWP\KadenceBlocks\Uplink\Uplink_Provider;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* The Core Kadence Blocks Application, with container support.
|
||||
*/
|
||||
final class App {
|
||||
|
||||
private static self $instance;
|
||||
|
||||
/**
|
||||
* @var Container
|
||||
*/
|
||||
private Container $container;
|
||||
|
||||
/**
|
||||
* Add any custom providers here.
|
||||
*
|
||||
* @note The order is important.
|
||||
*
|
||||
* @var class-string<Providable>
|
||||
*/
|
||||
private $providers = [
|
||||
Log_Provider::class,
|
||||
Database_Provider::class,
|
||||
Asset_Provider::class,
|
||||
Uplink_Provider::class,
|
||||
Harbor_Provider::class,
|
||||
Health_Provider::class,
|
||||
Admin_Provider::class,
|
||||
Image_Downloader_Provider::class,
|
||||
Optimizer_Provider::class,
|
||||
Cache_Provider::class,
|
||||
Shutdown_Provider::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Container $container
|
||||
*/
|
||||
private function __construct(
|
||||
Container $container
|
||||
) {
|
||||
$this->container = $container;
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Container|null $container
|
||||
*
|
||||
* @return self
|
||||
* @throws InvalidArgumentException If no container is provided.
|
||||
*/
|
||||
public static function instance( ?Container $container = null ): App {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
if ( ! $container ) {
|
||||
throw new InvalidArgumentException( 'You need to provide a concrete Contracts\Container instance!' );
|
||||
}
|
||||
|
||||
self::$instance = new self( $container );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function container(): Container {
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
private function init(): void {
|
||||
$this->container->bind( Container::class, $this->container );
|
||||
$this->container->bind( ContainerInterface::class, $this->container );
|
||||
$this->container->singleton( Dot::class, new Dot() );
|
||||
|
||||
foreach ( $this->providers as $provider ) {
|
||||
$this->container->register( $provider );
|
||||
}
|
||||
}
|
||||
|
||||
private function __clone() {
|
||||
}
|
||||
|
||||
public function __wakeup(): void {
|
||||
throw new RuntimeException( 'method not implemented' );
|
||||
}
|
||||
|
||||
public function __sleep(): array {
|
||||
throw new RuntimeException( 'method not implemented' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Asset;
|
||||
|
||||
final class Asset {
|
||||
|
||||
/**
|
||||
* @var string The URL to the plugin's main folder.
|
||||
*/
|
||||
private string $plugin_url;
|
||||
|
||||
/**
|
||||
* @param string $plugin_url The URL to the plugin's main folder.
|
||||
*/
|
||||
public function __construct( string $plugin_url ) {
|
||||
$this->plugin_url = $plugin_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset URL.
|
||||
*
|
||||
* @param string $path A relative path to an asset.
|
||||
*
|
||||
* @return string The full URL to the asset.
|
||||
*/
|
||||
public function get_url( string $path = '' ): string {
|
||||
return $this->plugin_url . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the asset meta.
|
||||
*
|
||||
* @param string $path The filepath without extension, e.g. dist/hello.
|
||||
*
|
||||
* @return array{dependencies?: string[], version?: string };
|
||||
*/
|
||||
public function get_meta( string $path = '' ): array {
|
||||
return kadence_blocks_get_asset_file( $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a script.
|
||||
*
|
||||
* @param string $name The name of the script.
|
||||
* @param string $path The relative path to the script, without extension, e.g. build/settings.
|
||||
* @param string $ext The file extension without a period.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_script( string $name, string $path, string $ext = 'js' ): void {
|
||||
$meta = $this->get_meta( $path );
|
||||
|
||||
wp_enqueue_script(
|
||||
$name,
|
||||
$this->get_url( "$path.$ext" ),
|
||||
$meta['dependencies'] ?? [],
|
||||
$meta['version'] ?? '',
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Asset;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
|
||||
final class Asset_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function register(): void {
|
||||
$this->container->when( Asset::class )
|
||||
->needs( '$plugin_url' )
|
||||
->give( static fn(): string => KADENCE_BLOCKS_URL );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Cache;
|
||||
|
||||
/**
|
||||
* Caches AI Related files.
|
||||
*/
|
||||
final class Ai_Cache extends Block_Library_Cache {
|
||||
|
||||
/**
|
||||
* Create a hashed file name from provided identifier.
|
||||
*
|
||||
* @param mixed $identifier Unique data to identify this file.
|
||||
*
|
||||
* @return string
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function filename( $identifier ): string {
|
||||
return $this->hasher->hash( [
|
||||
'kadence-ai-generated-content',
|
||||
$identifier,
|
||||
] ) . $this->ext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Cache;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Hasher;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\Shutdown\Contracts\Terminable;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Storage\Contracts\Storage;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Storage\Exceptions\StorageException;
|
||||
|
||||
/**
|
||||
* Caches Block Library files.
|
||||
*/
|
||||
class Block_Library_Cache implements Terminable {
|
||||
|
||||
/**
|
||||
* @var Storage
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* @var Hasher
|
||||
*/
|
||||
protected $hasher;
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The filename extension for all files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $ext;
|
||||
|
||||
/**
|
||||
* The data to be cached on shutdown, indexed by a filename.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* @param Storage $storage The file storage library.
|
||||
* @param Hasher $hasher The hashing library.
|
||||
* @param Config $config The cache configuration.
|
||||
* @param LoggerInterface $logger The logger, enabled if WP_DEBUG is set to true.
|
||||
* @param string $ext The file extension all files will be saved with.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
Storage $storage,
|
||||
Hasher $hasher,
|
||||
Config $config,
|
||||
LoggerInterface $logger,
|
||||
string $ext = '.json'
|
||||
) {
|
||||
$this->storage = $storage;
|
||||
$this->hasher = $hasher;
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
$this->ext = $ext;
|
||||
|
||||
if ( ! str_starts_with( $ext, '.' ) || str_starts_with( $ext, '..' ) ) {
|
||||
throw new InvalidArgumentException( 'The $ext must start with a single period' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime data to be cached when the WordPress shutdown action occurs.
|
||||
*
|
||||
* @param mixed $identifier Unique data to identify this file.
|
||||
* @param mixed $data The data to store.
|
||||
*
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function cache( $identifier, $data ): void {
|
||||
$identifier = $this->filename( $identifier );
|
||||
|
||||
$this->items[ $identifier ] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write cache file on shutdown.
|
||||
*
|
||||
* @action shutdown
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function terminate(): void {
|
||||
$this->write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the data to the file system cache on WordPress's shutdown action.
|
||||
*
|
||||
* @action shutdown
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function write(): void {
|
||||
if ( ! isset( $this->items ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $this->items as $filename => $data ) {
|
||||
if ( $this->storage->has( $filename ) ) {
|
||||
$this->logger->debug( 'Filename already exists, deleting file...', [
|
||||
'filename' => $filename,
|
||||
] );
|
||||
|
||||
try {
|
||||
$this->storage->delete( $filename );
|
||||
} catch ( StorageException $e ) {
|
||||
$this->logger->error( 'Error deleting existing cache file', [
|
||||
'filename' => $filename,
|
||||
'exception' => $e->getMessage(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logger->debug( sprintf( 'Writing cache file: %s', $filename ) );
|
||||
|
||||
$this->storage->put( $filename, $data );
|
||||
} catch ( StorageException $e ) {
|
||||
$this->logger->error( 'Error saving cache file', [
|
||||
'filename' => $filename,
|
||||
'exception' => $e->getMessage(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
unset( $this->items );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file from the cache.
|
||||
*
|
||||
* @param mixed $identifier Unique data to identify this file.
|
||||
*
|
||||
* @return string The file contents.
|
||||
* @throws InvalidArgumentException
|
||||
* @throws \KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Storage\Exceptions\NotFoundException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function get( $identifier ): string {
|
||||
$identifier = $this->filename( $identifier );
|
||||
$content = $this->storage->get( $identifier );
|
||||
|
||||
$this->logger->debug( sprintf( 'Found cache file: %s', $identifier ) );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hashed file name from provided identifier.
|
||||
*
|
||||
* @param mixed $identifier Unique data to identify this file.
|
||||
*
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function filename( $identifier ): string {
|
||||
return $this->hasher->hash( [
|
||||
$this->config->base_url(),
|
||||
$this->config->base_path(),
|
||||
$identifier,
|
||||
] ) . $this->ext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Cache;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Storage\Contracts\Storage;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Storage\Drivers\LocalStorage;
|
||||
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Filesystem;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
|
||||
final class Cache_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function register(): void {
|
||||
$base_path = apply_filters( 'kadence_block_library_local_data_base_path', trailingslashit( wp_get_upload_dir()['basedir'] ) );
|
||||
$base_url = apply_filters( 'kadence_block_library_local_data_base_url', content_url() );
|
||||
|
||||
$this->container->singleton( Config::class, new Config( $base_path, $base_url ) );
|
||||
|
||||
$this->register_block_library_storage();
|
||||
$this->register_ai_storage();
|
||||
}
|
||||
|
||||
private function register_block_library_storage(): void {
|
||||
$library_subfolder = apply_filters( 'kadence_block_library_local_data_subfolder_name', 'kadence_blocks_library' );
|
||||
$path = $this->container->get( Config::class )->base_path() . $library_subfolder;
|
||||
|
||||
$this->container->when( Block_Library_Cache::class )
|
||||
->needs( Storage::class )
|
||||
->give( new LocalStorage( $this->container->get( Filesystem::class ), $path ) );
|
||||
|
||||
$this->container->singleton( Block_Library_Cache::class, Block_Library_Cache::class );
|
||||
}
|
||||
|
||||
private function register_ai_storage(): void {
|
||||
$ai_subfolder = apply_filters( 'kadence_block_ai_local_data_subfolder_name', 'kadence_ai' );
|
||||
$path = $this->container->get( Config::class )->base_path() . $ai_subfolder;
|
||||
|
||||
$this->container->when( Ai_Cache::class )
|
||||
->needs( Storage::class )
|
||||
->give( new LocalStorage( $this->container->get( Filesystem::class ), $path ) );
|
||||
|
||||
$this->container->singleton( Ai_Cache::class, Ai_Cache::class );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Cache;
|
||||
|
||||
/**
|
||||
* Cache configuration.
|
||||
*/
|
||||
final class Config {
|
||||
|
||||
/**
|
||||
* The base server path to the uploads folder.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_path;
|
||||
|
||||
/**
|
||||
* The base URL to the uploads folder.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url;
|
||||
|
||||
public function __construct(
|
||||
string $base_path,
|
||||
string $base_url
|
||||
) {
|
||||
$this->base_path = $base_path;
|
||||
$this->base_url = $base_url;
|
||||
}
|
||||
|
||||
public function base_path(): string {
|
||||
return $this->base_path;
|
||||
}
|
||||
|
||||
public function base_url(): string {
|
||||
return $this->base_url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace KadenceWP\KadenceBlocks;
|
||||
|
||||
use KadenceWP\KadenceBlocks\lucatume\DI52\Container as DI52Container;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ContainerContract\ContainerInterface;
|
||||
|
||||
class Container implements ContainerInterface {
|
||||
|
||||
/**
|
||||
* @var DI52Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Container constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->container = new DI52Container();
|
||||
}
|
||||
|
||||
public function container(): DI52Container {
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function bind( string $id, $implementation = null, ?array $afterBuildMethods = null ) {
|
||||
$this->container->bind( $id, $implementation, $afterBuildMethods );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function get( string $id ) {
|
||||
return $this->container->get( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function has( string $id ) {
|
||||
return $this->container->has( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function singleton( string $id, $implementation = null, ?array $afterBuildMethods = null ) {
|
||||
$this->container->singleton( $id, $implementation, $afterBuildMethods );
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer all other calls to the container object.
|
||||
*/
|
||||
public function __call( $name, $args ) {
|
||||
return $this->container->{$name}( ...$args );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Database\Optimizer_Table;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Database\Viewport_Hash_Table;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\Database\Exceptions\DatabaseQueryException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\DB;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Schema\Config;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Schema\Register;
|
||||
|
||||
final class Database_Provider extends Provider {
|
||||
|
||||
public const SCHEMA_TABLES = 'kadence_blocks.database.schema_tables';
|
||||
|
||||
/**
|
||||
* Configure the stellarwp/schema library.
|
||||
*
|
||||
* @throws DatabaseQueryException If we failed to create or update the database tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
Config::set_container( $this->container );
|
||||
Config::set_db( DB::class );
|
||||
|
||||
// Add all schema tables to be registered here.
|
||||
$this->container->setVar(
|
||||
self::SCHEMA_TABLES,
|
||||
[
|
||||
Viewport_Hash_Table::class,
|
||||
Optimizer_Table::class,
|
||||
]
|
||||
);
|
||||
|
||||
try {
|
||||
Register::tables( $this->container->getVar( self::SCHEMA_TABLES ) );
|
||||
} catch ( DatabaseQueryException $e ) {
|
||||
$this->container->get( LoggerInterface::class )->emergency(
|
||||
'Unable to create or update database tables',
|
||||
[
|
||||
'query_errors' => $e->getQueryErrors(),
|
||||
'query' => $e->getQuery(),
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\DB;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\QueryBuilder\QueryBuilder;
|
||||
|
||||
/**
|
||||
* A query wrapper for the StellarWP DB instance using a specific
|
||||
* table.
|
||||
*
|
||||
* This class should be extended and configured for a specific table
|
||||
* via a Provider.
|
||||
*/
|
||||
abstract class Query {
|
||||
|
||||
/**
|
||||
* The WordPress table name without a prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $table;
|
||||
|
||||
/**
|
||||
* @param string $table The WordPress table name without a prefix.
|
||||
*/
|
||||
public function __construct( string $table ) {
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current table being used.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function table(): string {
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table name with prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function table_with_prefix(): string {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . $this->table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder for this table.
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function qb(): QueryBuilder {
|
||||
return DB::table( $this->table );
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for wpdb::get_var
|
||||
*
|
||||
* @see \wpdb::get_var()
|
||||
*
|
||||
* @param string|null $query Optional. SQL query. Defaults to null, use the result from the
|
||||
* previous query.
|
||||
* @param int $x Optional. Column of value to return. Indexed from 0. Default 0.
|
||||
* @param int $y Optional. Row of value to return. Indexed from 0. Default 0.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_var( ?string $query = null, int $x = 0, int $y = 0 ): ?string {
|
||||
return DB::get_var( $query, $x, $y );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Harbor\Actions;
|
||||
|
||||
/**
|
||||
* Provides the list of Kadence plugins that integrate with Harbor.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Get_Known_Plugins {
|
||||
|
||||
/**
|
||||
* Returns a map of all known Kadence plugin slugs to their metadata.
|
||||
*
|
||||
* page_url should be a full URL and should point to the plugin's
|
||||
* license settings page.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @return array<string, array{name: string, page_url: string}>
|
||||
*/
|
||||
public function __invoke(): array {
|
||||
$default_page = admin_url( 'admin.php?page=kadence-blocks' );
|
||||
|
||||
return [
|
||||
'kadence-blocks' => [
|
||||
'name' => 'Kadence Blocks',
|
||||
'page_url' => $default_page,
|
||||
],
|
||||
'kadence-blocks-pro' => [
|
||||
'name' => 'Kadence Blocks Pro',
|
||||
'page_url' => $default_page,
|
||||
],
|
||||
'kadence-creative-kit' => [
|
||||
'name' => 'Kadence Creative Kit',
|
||||
'page_url' => admin_url( 'admin.php?page=kadence-blocks-home&license=show' ),
|
||||
],
|
||||
'kadence-insights' => [
|
||||
'name' => 'Kadence Insights',
|
||||
'page_url' => admin_url( 'admin.php?page=kadence-insights-settings&license=show' ),
|
||||
],
|
||||
'kadence-shop-kit' => [
|
||||
'name' => 'Kadence Shop Kit',
|
||||
'page_url' => admin_url( 'admin.php?page=kadence-shop-kit-settings&license=show' ),
|
||||
],
|
||||
'kadence-galleries' => [
|
||||
'name' => 'Kadence Galleries',
|
||||
'page_url' => admin_url('edit.php?post_type=kt_gallery&page=kadence-galleries-settings'),
|
||||
],
|
||||
'kadence-conversions' => [
|
||||
'name' => 'Kadence Conversions',
|
||||
'page_url' => admin_url('admin.php?page=kadence-conversion-settings'),
|
||||
],
|
||||
'kadence-captcha' => [
|
||||
'name' => 'Kadence Captcha',
|
||||
'page_url' => admin_url( 'admin.php?page=kadence-recaptcha-settings&license=show' ),
|
||||
],
|
||||
'kadence-pattern-hub' => [
|
||||
'name' => 'Kadence Pattern Hub',
|
||||
'page_url' => admin_url('edit.php?post_type=kadence_cloud&page=kadence-cloud-settings&license=show'),
|
||||
],
|
||||
'kadence-white-label' => [
|
||||
'name' => 'Kadence White Label',
|
||||
'page_url' => admin_url( 'admin.php?page=kadence-white-label-settings' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Harbor\Actions;
|
||||
|
||||
/**
|
||||
* Renders the Harbor (Liquid Web Software Manager) notice on legacy Uplink license forms.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Render_Harbor_License_Notice {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $product_name;
|
||||
|
||||
/**
|
||||
* @param string $product_name
|
||||
*/
|
||||
public function __construct( string $product_name ) {
|
||||
$this->product_name = $product_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Harbor notice below the save button on a Kadence Uplink license field.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(): void {
|
||||
$url = lw_harbor_get_license_page_url();
|
||||
if ( empty( $url ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<hr style="margin: 20px 0;"/>
|
||||
<h4><span class="dashicons dashicons-info" style="vertical-align: middle; margin-right: 4px;"></span><?php esc_html_e( 'Liquid Web Software Manager', 'kadence-blocks' ); ?></h4>
|
||||
<p class="tooltip description"><?php echo wp_kses(
|
||||
sprintf(
|
||||
/* translators: 1: product name, 2: URL to the Liquid Web Software Manager page. */
|
||||
__( '%1$s is now part of Liquid Web\'s software offerings. This field is still available for managing legacy licenses from your previous %1$s account. If you purchased a new plan through Liquid Web, your products are managed through the <a href="%2$s">Liquid Web Software Manager</a>.', 'kadence-blocks' ),
|
||||
esc_html( $this->product_name ),
|
||||
esc_url( $url )
|
||||
),
|
||||
[ 'a' => [ 'href' => [] ] ]
|
||||
); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Harbor\Actions;
|
||||
|
||||
use function KadenceWP\KadenceBlocks\StellarWP\Uplink\get_license_key;
|
||||
use function KadenceWP\KadenceBlocks\StellarWP\Uplink\get_resource;
|
||||
|
||||
/**
|
||||
* Reports Kadence's legacy Uplink licenses into Harbor's unified license listing.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Report_Legacy_Licenses {
|
||||
|
||||
/**
|
||||
* Reports legacy Uplink-managed Kadence licenses to Harbor so they appear
|
||||
* in the unified license UI.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param array $licenses Existing legacy licenses from other plugins.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __invoke( array $licenses ): array {
|
||||
$reported_keys = [];
|
||||
|
||||
foreach ( ( new Get_Known_Plugins() )() as $slug => $plugin ) {
|
||||
$key = get_license_key( $slug );
|
||||
|
||||
if ( empty( $key ) || isset( $reported_keys[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resource = get_resource( $slug );
|
||||
|
||||
if ( ! $resource ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reported_keys[ $key ] = true;
|
||||
|
||||
$is_active = $resource->has_valid_license();
|
||||
|
||||
$licenses[] = [
|
||||
'key' => $key,
|
||||
'slug' => $slug,
|
||||
'name' => $plugin['name'],
|
||||
'product' => 'kadence',
|
||||
'is_active' => $is_active,
|
||||
'page_url' => esc_url( $plugin['page_url'] ),
|
||||
];
|
||||
}
|
||||
|
||||
return $licenses;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Harbor\Actions;
|
||||
|
||||
/**
|
||||
* Suppresses legacy Kadence add-on "license not activated" admin notices in favor of Harbor's unified UI.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Suppress_Legacy_Inactive_Notices {
|
||||
|
||||
/**
|
||||
* Removes legacy "not activated" admin notices from all Kadence add-ons.
|
||||
*
|
||||
* Each add-on registers its inactive_notice callback during plugins_loaded,
|
||||
* which runs after Harbor_Provider::register(). Hooking to admin_init ensures
|
||||
* all add-on callbacks are already registered before we remove them.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(): void {
|
||||
global $wp_filter;
|
||||
|
||||
if ( empty( $wp_filter['admin_notices'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $wp_filter['admin_notices']->callbacks as $priority => $callbacks ) {
|
||||
foreach ( $callbacks as $key => $callback ) {
|
||||
$function = $callback['function'];
|
||||
|
||||
if ( ! is_array( $function )
|
||||
|| ! is_object( $function[0] )
|
||||
|| $function[1] !== 'inactive_notice'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( strpos( get_class( $function[0] ), 'KadenceWP\\' ) === 0 ) {
|
||||
unset( $wp_filter['admin_notices']->callbacks[ $priority ][ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Harbor;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Harbor\Actions\Get_Known_Plugins;
|
||||
use KadenceWP\KadenceBlocks\Harbor\Actions\Render_Harbor_License_Notice;
|
||||
use KadenceWP\KadenceBlocks\Harbor\Actions\Report_Legacy_Licenses;
|
||||
use KadenceWP\KadenceBlocks\Harbor\Actions\Suppress_Legacy_Inactive_Notices;
|
||||
use KadenceWP\KadenceBlocks\LiquidWeb\Harbor\Config as HarborConfig;
|
||||
use KadenceWP\KadenceBlocks\LiquidWeb\Harbor\Harbor;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use ITSEC_Core;
|
||||
|
||||
/**
|
||||
* Wires the Harbor (LiquidWeb unified license) integration into Kadence Blocks.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Harbor_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
HarborConfig::set_plugin_basename( KADENCE_BLOCKS_PLUGIN_BASENAME );
|
||||
HarborConfig::set_container( $this->container );
|
||||
|
||||
add_filter( 'lw_harbor/premium_plugin_exists', [ $this, 'register_premium_plugin_exists' ] );
|
||||
|
||||
Harbor::init();
|
||||
|
||||
lw_harbor_register_submenu( 'kadence-blocks' );
|
||||
|
||||
add_filter( 'lw-harbor/legacy_licenses', new Report_Legacy_Licenses() );
|
||||
add_filter( 'kadence_blocks_ai_disabled', [ $this, 'is_ai_disabled' ] );
|
||||
add_filter( 'kadence_blocks_ai_disabled_message', [ $this, 'ai_disabled_message' ] );
|
||||
|
||||
foreach ( ( new Get_Known_Plugins() )() as $slug => $plugin ) {
|
||||
add_action(
|
||||
"stellarwp/uplink/{$slug}/license_field_after_form",
|
||||
new Render_Harbor_License_Notice( $plugin['name'] )
|
||||
);
|
||||
|
||||
add_filter( "stellarwp/uplink/{$slug}/plugin_notices", [ $this, 'suppress_inline_license_notices' ] );
|
||||
}
|
||||
|
||||
add_action( 'admin_init', new Suppress_Legacy_Inactive_Notices() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables Kadence AI for new Harbor customers who don't have legacy AI access.
|
||||
*
|
||||
* @param bool $disabled Whether AI is already disabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ai_disabled( bool $disabled ): bool {
|
||||
if ( $disabled ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! kadence_blocks_is_legacy_license_authorized() && lw_harbor_is_product_license_active( 'kadence' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the AI disabled message for Harbor-licensed customers.
|
||||
*
|
||||
* @param string $message The default disabled message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ai_disabled_message( string $message ): string {
|
||||
if ( kadence_blocks_is_legacy_license_authorized() ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
if ( lw_harbor_is_product_license_active( 'kadence' ) ) {
|
||||
return __( 'We\'re building something new. Kadence AI as you know it is no longer available for new activations — but great things are on the way. Stay tuned for what\'s next.', 'kadence-blocks' );
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppresses the StellarWP Uplink inline license notice on the WP plugins
|
||||
* page for LiquidWeb customers, who manage licensing through the unified key.
|
||||
*
|
||||
* This was intentionally kept simple for any unified key instead of Kadence specific so that plugins can continue offloading notices to the Harbor library.
|
||||
*
|
||||
* @param array<string, array{slug: string, message_row_html: string}> $notices
|
||||
*
|
||||
* @return array<string, array{slug: string, message_row_html: string}>
|
||||
*/
|
||||
public function suppress_inline_license_notices( array $notices ): array {
|
||||
return lw_harbor_has_unified_license_key() ? [] : $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the premium plugin existence callbacks.
|
||||
*
|
||||
* @since 3.7.2
|
||||
*
|
||||
* @param bool $exists Whether a premium plugin exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function register_premium_plugin_exists( bool $exists ): bool {
|
||||
if ( $exists ) {
|
||||
// It already exists.
|
||||
return true;
|
||||
}
|
||||
|
||||
$premium_constants = [
|
||||
'KTP_PLUGIN_FILE',
|
||||
'KADENCE_WOO_EXTRAS_VERSION',
|
||||
'SOLIDWP_BACKUPS_PLUGIN_FILE',
|
||||
'KADENCE_CONVERSIONS_FILE',
|
||||
'KADENCE_INSIGHTS_FILE',
|
||||
'KADENCE_WHITE_VERSION',
|
||||
];
|
||||
|
||||
foreach ( $premium_constants as $premium_constant ) {
|
||||
if ( ! defined( $premium_constant ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kadence Security Pro does not have a constant.
|
||||
if (
|
||||
class_exists( ITSEC_Core::class ) &&
|
||||
ITSEC_Core::get_install_type() === 'pro'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class Hasher {
|
||||
|
||||
/**
|
||||
* The hashing algorithm to use.
|
||||
*
|
||||
* If on PHP8.1+, we'll use xxh128.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $algo;
|
||||
|
||||
/**
|
||||
* @param string $algo The hashing algorithm to use.
|
||||
*
|
||||
* @see \hash_algos()
|
||||
*/
|
||||
public function __construct( string $algo = 'md5' ) {
|
||||
$this->algo = $algo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash from different types of data.
|
||||
*
|
||||
* @param string|object|array|int|float $data The data to hash.
|
||||
* @param bool $binary Output in raw binary.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws InvalidArgumentException|RuntimeException
|
||||
*/
|
||||
public function hash( $data, bool $binary = false ): string {
|
||||
if ( $data === null ) {
|
||||
throw new InvalidArgumentException( '$data cannot be null.' );
|
||||
}
|
||||
|
||||
$data = is_scalar( $data ) ? (string) $data : (string) json_encode( $data );
|
||||
|
||||
if ( strlen( $data ) <= 0 ) {
|
||||
throw new RuntimeException( 'Cannot hash an empty data string. Perhaps JSON encoding failed?' );
|
||||
}
|
||||
|
||||
return hash( $this->algo, $data, $binary );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Health;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Uplink\Notice\Notice;
|
||||
|
||||
/**
|
||||
* Check the Health aka the status of requirements, dependencies or anything that
|
||||
* could affect the running of the plugin.
|
||||
*/
|
||||
final class Health_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function register(): void {
|
||||
/*
|
||||
* An array indexed by PHP function names to check are enabled and the Notice
|
||||
* type to render if they aren't.
|
||||
*
|
||||
* Adjust as needed.
|
||||
*/
|
||||
$this->container->when( Required_Function_Verifier::class )
|
||||
->needs( '$function_map' )
|
||||
->give( static function (): array {
|
||||
return [
|
||||
'error_log' => Notice::ERROR,
|
||||
'curl_multi_exec' => Notice::WARNING,
|
||||
];
|
||||
} );
|
||||
|
||||
add_action(
|
||||
'admin_notices',
|
||||
$this->container->callback( Required_Function_Verifier::class, 'verify_functions' )
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Health;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Notice\Notice_Handler;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Uplink\Notice\Notice;
|
||||
|
||||
final class Required_Function_Verifier {
|
||||
|
||||
/**
|
||||
* An array indexed by PHP function names to check are enabled and the Notice
|
||||
* type to render if they aren't.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $function_map;
|
||||
|
||||
private Notice_Handler $notice_handler;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $function_map
|
||||
*/
|
||||
public function __construct( array $function_map, Notice_Handler $notice_handler ) {
|
||||
$this->function_map = $function_map;
|
||||
$this->notice_handler = $notice_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* When on the Kadence Blocks settings page, show notices if any functions are disabled.
|
||||
*
|
||||
* @hook admin_notices
|
||||
*/
|
||||
public function verify_functions(): void {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( $screen && $screen->id !== 'toplevel_page_kadence-blocks' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $this->function_map as $function => $type ) {
|
||||
if ( function_exists( $function ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->notice_handler->add( new Notice(
|
||||
$type,
|
||||
// translators: %1$s is the function name, %2$s is "required" or "suggested".
|
||||
sprintf(
|
||||
__( 'The "%1$s" function is disabled via PHP and is %2$s by Kadence Blocks. Ask your administrator to enable it.', 'kadence-blocks' ),
|
||||
$function,
|
||||
$type === Notice::ERROR ? __( 'required', 'kadence-blocks' ) : __( 'suggested', 'kadence-blocks' ),
|
||||
),
|
||||
true
|
||||
) );
|
||||
}
|
||||
|
||||
$this->notice_handler->display();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Home;
|
||||
|
||||
use function KadenceWP\KadenceBlocks\StellarWP\Uplink\build_auth_url;
|
||||
use function KadenceWP\KadenceBlocks\StellarWP\Uplink\get_license_domain;
|
||||
|
||||
/**
|
||||
* View model that produces the kadenceHomeParams.homeContent payload.
|
||||
*
|
||||
* All home page copy lives here so changes require no JavaScript deployment.
|
||||
* The primaryCtaUrl for the authorized banner scenario is intentionally empty —
|
||||
* the frontend interprets an empty URL as "open the AI wizard".
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
final class Home_Content_View_Model {
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public function exports(): array {
|
||||
$is_authorized = ! kadence_blocks_is_ai_disabled() && kadence_blocks_is_legacy_license_authorized();
|
||||
$is_liquid_web = ! $is_authorized && lw_harbor_is_product_license_active( 'kadence' );
|
||||
|
||||
return [
|
||||
'bannerConfig' => $this->banner_config( $is_authorized, $is_liquid_web ),
|
||||
'actionCards' => $this->action_cards( $is_authorized ),
|
||||
'knowledgeBase' => $this->knowledge_base( $is_authorized ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param bool $is_authorized Whether the current site has an authorized Kadence license.
|
||||
* @param bool $is_liquid_web Whether the current site is a Liquid Web customer.
|
||||
*
|
||||
* @return array{
|
||||
* heading: string,
|
||||
* body: string,
|
||||
* primaryCtaText: string,
|
||||
* primaryCtaUrl: string,
|
||||
* secondaryCtaText?: string,
|
||||
* secondaryCtaUrl?: string,
|
||||
* }
|
||||
*/
|
||||
private function banner_config( bool $is_authorized, bool $is_liquid_web ): array {
|
||||
// (new) customer scenario (liquid web).
|
||||
if ( $is_liquid_web ) {
|
||||
return [
|
||||
'heading' => __( 'Kadence AI is evolving.', 'kadence-blocks' ),
|
||||
'body' => __( "We're building something new. Kadence AI as you know it is no longer available for new activations — but great things are on the way. Stay tuned for what's next.", 'kadence-blocks' ),
|
||||
'primaryCtaText' => __( 'Learn More', 'kadence-blocks' ),
|
||||
'primaryCtaUrl' => 'https://www.kadencewp.com/kadence-blocks/',
|
||||
];
|
||||
}
|
||||
|
||||
// legacy customer scenario.
|
||||
if ( $is_authorized ) {
|
||||
return [
|
||||
'heading' => __( 'Kadence is better with AI.', 'kadence-blocks' ),
|
||||
'body' => __( "Elevate your web development game with Kadence AI. Supercharge your pattern and page library's potential with tailored content — get building pages in no time. You have AI credits remaining on your account.", 'kadence-blocks' ),
|
||||
'primaryCtaText' => __( 'Get Started with Kadence AI', 'kadence-blocks' ),
|
||||
'primaryCtaUrl' => '',
|
||||
'secondaryCtaText' => __( 'Manage AI Credits', 'kadence-blocks' ),
|
||||
'secondaryCtaUrl' => esc_url( build_auth_url( apply_filters( 'kadence-blocks-auth-slug', 'kadence-blocks' ), get_license_domain() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
// unknown customer scenario.
|
||||
return [
|
||||
'heading' => __( 'Kadence AI is evolving.', 'kadence-blocks' ),
|
||||
'body' => __("If you're using a legacy Kadence plan, activate your license to access your included features. If you're on a new plan, Kadence AI is currently being reimagined to deliver a more powerful experience. During this transition, it's only available on previous Kadence plans.", 'kadence-blocks' ),
|
||||
'primaryCtaText' => __( 'Activate License Key', 'kadence-blocks' ),
|
||||
'primaryCtaUrl' => esc_url( build_auth_url( apply_filters( 'kadence-blocks-auth-slug', 'kadence-blocks' ), get_license_domain() ) ),
|
||||
'secondaryCtaText' => __( 'Learn More', 'kadence-blocks' ),
|
||||
'secondaryCtaUrl' => 'https://www.kadencewp.com/kadence-blocks/',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param bool $is_authorized Whether the current site has an authorized Kadence license.
|
||||
*
|
||||
* @return array{
|
||||
* title: string,
|
||||
* showAiIcon: bool,
|
||||
* cards: array<int, array{heading: string, content: string, link: string, variant: string}>,
|
||||
* }
|
||||
*/
|
||||
private function action_cards( bool $is_authorized ): array {
|
||||
if ( $is_authorized ) {
|
||||
return [
|
||||
'title' => __( 'Start building with Kadence AI.', 'kadence-blocks' ),
|
||||
'showAiIcon' => true,
|
||||
'cards' => [
|
||||
[
|
||||
'heading' => __( 'Build a page with AI-powered patterns', 'kadence-blocks' ),
|
||||
'content' => __( 'Take your site further with hundreds of beautiful patterns filled with custom content developed just for your site.', 'kadence-blocks' ),
|
||||
'link' => admin_url( 'post-new.php?post_type=page' ),
|
||||
'variant' => 'blue',
|
||||
],
|
||||
[
|
||||
'heading' => __( 'Get started with full pages', 'kadence-blocks' ),
|
||||
'content' => __( 'Choose from a variety of pages featuring exclusively tailored content for your site.', 'kadence-blocks' ),
|
||||
'link' => admin_url( 'post-new.php?post_type=page' ),
|
||||
'variant' => 'green',
|
||||
],
|
||||
[
|
||||
'heading' => __( 'Fine-tune your content', 'kadence-blocks' ),
|
||||
'content' => __( 'Write your own prompts to generate AI content from scratch or fine-tune existing copy.', 'kadence-blocks' ),
|
||||
'link' => '',
|
||||
'variant' => 'yellow',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => __( 'Start building with Kadence', 'kadence-blocks' ),
|
||||
'showAiIcon' => false,
|
||||
'cards' => [
|
||||
[
|
||||
'heading' => __( 'Build a page using pre-designed patterns', 'kadence-blocks' ),
|
||||
'content' => __( "Elevate your site's design with hundreds of beautiful patterns, customized with your site's style.", 'kadence-blocks' ),
|
||||
// TODO: add query param to open Design Library on Patterns tab once URL param is established.
|
||||
'link' => admin_url( 'post-new.php?post_type=page' ),
|
||||
'variant' => 'blue',
|
||||
],
|
||||
[
|
||||
'heading' => __( 'Get started with full pages', 'kadence-blocks' ),
|
||||
'content' => __( "Kickstart your site with a variety of pre-designed page layouts, customized with your site's style.", 'kadence-blocks' ),
|
||||
// TODO: add query param to open Design Library on Pages tab once URL param is established.
|
||||
'link' => admin_url( 'post-new.php?post_type=page' ),
|
||||
'variant' => 'green',
|
||||
],
|
||||
[
|
||||
'heading' => __( 'Start from scratch', 'kadence-blocks' ),
|
||||
'content' => __( "Build your website from scratch, using Kadence's blocks for layout, content and interactions.", 'kadence-blocks' ),
|
||||
'link' => admin_url( 'post-new.php?post_type=page' ),
|
||||
'variant' => 'yellow',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param bool $is_authorized Whether the current site has an authorized Kadence license.
|
||||
*
|
||||
* @return array{
|
||||
* heading: string,
|
||||
* articles: array<int, array{category: string, heading: string, description: string, link: string, linkTarget: string}>,
|
||||
* }
|
||||
*/
|
||||
private function knowledge_base( bool $is_authorized ): array {
|
||||
return [
|
||||
'heading' => __( 'Need Help Getting Started?', 'kadence-blocks' ),
|
||||
'articles' => $is_authorized ? $this->knowledge_base_authorized() : $this->knowledge_base_default(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @return array<int, array{category: string, heading: string, description: string, link: string, linkTarget: string}>
|
||||
*/
|
||||
private function knowledge_base_authorized(): array {
|
||||
return [
|
||||
[
|
||||
'category' => __( 'Kadence AI', 'kadence-blocks' ),
|
||||
'heading' => __( 'Update AI Settings', 'kadence-blocks' ),
|
||||
'description' => __( 'Update Kadence AI settings. Regenerate contexts for patterns and pages to reflect your updated needs.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/design-libary-changing-ai-details/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence AI', 'kadence-blocks' ),
|
||||
'heading' => __( 'Customize Image Collections', 'kadence-blocks' ),
|
||||
'description' => __( 'Update your Design Library imagery using premade collections or create and customize your own.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/design-library-changing-ai-image-collections/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence Blocks', 'kadence-blocks' ),
|
||||
'heading' => __( 'Row Layout Block', 'kadence-blocks' ),
|
||||
'description' => __( 'Use the Row Layout block to improve the column functionality and create responsive post/page layouts.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/row-layout-block-2/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence Blocks', 'kadence-blocks' ),
|
||||
'heading' => __( 'Advanced Text Block', 'kadence-blocks' ),
|
||||
'description' => __( 'Use the Advanced Text block to add text to your page/post with advanced customization - now with AI.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/advanced-heading-block/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Support', 'kadence-blocks' ),
|
||||
'heading' => __( 'Need more help?', 'kadence-blocks' ),
|
||||
'description' => __( "Didn't find what you were looking for? Find more articles in our knowledge base.", 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @return array<int, array{category: string, heading: string, description: string, link: string, linkTarget: string}>
|
||||
*/
|
||||
private function knowledge_base_default(): array {
|
||||
return [
|
||||
[
|
||||
'category' => __( 'Kadence Blocks', 'kadence-blocks' ),
|
||||
'heading' => __( 'Using the Design Library', 'kadence-blocks' ),
|
||||
'description' => __( 'Use fully designed patterns and pages on your site with your own customizer settings - now with AI.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/how-to-control-the-kadence-design-library/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence Blocks', 'kadence-blocks' ),
|
||||
'heading' => __( 'Row Layout Block', 'kadence-blocks' ),
|
||||
'description' => __( 'Use the Row Layout block to improve the column functionality and create responsive post/page layouts.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/row-layout-block-2/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence Blocks', 'kadence-blocks' ),
|
||||
'heading' => __( 'Advanced Text Block', 'kadence-blocks' ),
|
||||
'description' => __( 'Use the Advanced Text block to add text to your page/post with advanced customization - now with AI.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/advanced-heading-block/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Kadence Blocks Pro', 'kadence-blocks' ),
|
||||
'heading' => __( 'Kadence Blocks Pro Plugin', 'kadence-blocks' ),
|
||||
'description' => __( 'Install and activate the Kadence Blocks Pro plugin, and get an overview of the Pro features available.', 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/docs/kadence-blocks/kadence-blocks-pro-plugin/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
[
|
||||
'category' => __( 'Support', 'kadence-blocks' ),
|
||||
'heading' => __( 'Need more help?', 'kadence-blocks' ),
|
||||
'description' => __( "Didn't find what you were looking for? Find more articles in our knowledge base.", 'kadence-blocks' ),
|
||||
'link' => 'https://www.kadencewp.com/help-center/',
|
||||
'linkTarget' => '_blank',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Log;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Monolog\Handler\AbstractHandler;
|
||||
use KadenceWP\KadenceBlocks\Monolog\Handler\ErrorLogHandler;
|
||||
use KadenceWP\KadenceBlocks\Monolog\Handler\NullHandler;
|
||||
use KadenceWP\KadenceBlocks\Monolog\Logger;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Log\Formatters\ColoredLineFormatter;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Log\LogLevel;
|
||||
|
||||
final class Log_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function register(): void {
|
||||
// Enable logging to the error log if WP_DEBUG is enabled and error_log is not listed in the php.ini/fpm disable_functions directive.
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
|
||||
/**
|
||||
* Filter the log level to use when debugging.
|
||||
*
|
||||
* @param string $log_level One of: debug, info, notice, warning, error, critical, alert, emergency
|
||||
*/
|
||||
$log_level = apply_filters( 'kadence_blocks_image_download_log_level', 'debug' );
|
||||
|
||||
$this->container->when( ColoredLineFormatter::class )
|
||||
->needs( '$dateFormat' )
|
||||
->give( 'd/M/Y:H:i:s O' );
|
||||
|
||||
$this->container->when( AbstractHandler::class )
|
||||
->needs( '$level' )
|
||||
->give( LogLevel::fromName( $log_level ) );
|
||||
|
||||
$this->container->when( ErrorLogHandler::class )
|
||||
->needs( '$level' )
|
||||
->give( LogLevel::fromName( $log_level ) );
|
||||
|
||||
$this->container->bind(
|
||||
LoggerInterface::class,
|
||||
function () {
|
||||
$logger = new Logger( 'kadence' );
|
||||
$handler = $this->container->get( ErrorLogHandler::class );
|
||||
$handler->setFormatter( $this->container->get( ColoredLineFormatter::class ) );
|
||||
$logger->pushHandler( $handler );
|
||||
|
||||
// Prefix logs.
|
||||
$logger->pushProcessor(
|
||||
static function ( array $record ): array {
|
||||
$record['message'] = '[Kadence Blocks]: ' . $record['message'];
|
||||
|
||||
return $record;
|
||||
}
|
||||
);
|
||||
|
||||
return $logger;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Disable logging.
|
||||
$this->container->bind(
|
||||
LoggerInterface::class,
|
||||
static function () {
|
||||
$logger = new Logger( 'null' );
|
||||
$logger->pushHandler( new NullHandler() );
|
||||
|
||||
return $logger;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Notice;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Uplink\Notice\Notice;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Uplink\Notice\Notice_Controller;
|
||||
|
||||
/**
|
||||
* Handles rendering multiple admin notices at once.
|
||||
*/
|
||||
final class Notice_Handler {
|
||||
|
||||
/**
|
||||
* Controller responsible for rendering notices.
|
||||
*/
|
||||
private Notice_Controller $controller;
|
||||
|
||||
/**
|
||||
* @var Notice[]
|
||||
*/
|
||||
private array $notices = [];
|
||||
|
||||
public function __construct( Notice_Controller $controller ) {
|
||||
$this->controller = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a notice to the stack.
|
||||
*
|
||||
* @param Notice $notice
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function add( Notice $notice ): self {
|
||||
$this->notices[] = $notice;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all notices in the stack.
|
||||
*/
|
||||
public function display(): void {
|
||||
if ( count( $this->notices ) <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $this->notices as $notice ) {
|
||||
$this->controller->render( $notice->toArray() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the notices.
|
||||
*
|
||||
* @return Notice[]
|
||||
*/
|
||||
public function all(): array {
|
||||
return $this->notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notices in the stack.
|
||||
*/
|
||||
public function clear(): self {
|
||||
$this->notices = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path_Factory;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\WebsiteAnalysis;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
|
||||
/**
|
||||
* A registry to fetch custom data from the WebsiteAnalysis DTO.
|
||||
*
|
||||
* @phpstan-type HtmlClassHeightMap array<string, float> Class attribute => height in pixels.
|
||||
*/
|
||||
class Analysis_Registry {
|
||||
|
||||
private const SECTIONS = 'sections';
|
||||
|
||||
/**
|
||||
* Memoization cache.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $cache = [];
|
||||
private ?Path $path = null;
|
||||
private ?WebsiteAnalysis $analysis = null;
|
||||
private Store $store;
|
||||
private bool $is_mobile;
|
||||
|
||||
public function __construct(
|
||||
Store $store,
|
||||
Path_Factory $path_factory,
|
||||
bool $is_mobile
|
||||
) {
|
||||
$this->store = $store;
|
||||
$this->is_mobile = $is_mobile;
|
||||
|
||||
try {
|
||||
$this->path = $path_factory->make();
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
$this->path = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the memoization cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush(): void {
|
||||
$this->analysis = null;
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the WebsiteAnalysis DTO based on the current path.
|
||||
*
|
||||
* @return WebsiteAnalysis|null
|
||||
*/
|
||||
public function get_analysis(): ?WebsiteAnalysis {
|
||||
if ( $this->analysis !== null ) {
|
||||
return $this->analysis;
|
||||
}
|
||||
|
||||
if ( ! $this->path ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->analysis = $this->store->get( $this->path );
|
||||
|
||||
return $this->analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this request is optimized.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_optimized(): bool {
|
||||
return $this->get_analysis() instanceof WebsiteAnalysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the above the fold background images for the current viewport.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_background_images(): array {
|
||||
$analysis = $this->get_analysis();
|
||||
|
||||
return $analysis
|
||||
? ( $this->is_mobile ? $analysis->mobile->backgroundImages : $analysis->desktop->backgroundImages )
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of the above the fold image URLs.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_critical_images(): array {
|
||||
$analysis = $this->get_analysis();
|
||||
|
||||
return $analysis
|
||||
? ( $this->is_mobile ? $analysis->mobile->criticalImages : $analysis->desktop->criticalImages )
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* For the current viewport, get all the "below the fold" sections that have a height
|
||||
* greater than 0.
|
||||
*
|
||||
* @return HtmlClassHeightMap
|
||||
*/
|
||||
public function get_valid_sections(): array {
|
||||
if ( isset( $this->cache[ self::SECTIONS ] ) ) {
|
||||
return $this->cache[ self::SECTIONS ];
|
||||
}
|
||||
|
||||
$analysis = $this->get_analysis();
|
||||
|
||||
if ( ! $analysis ) {
|
||||
$this->cache[ self::SECTIONS ] = [];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$sections = $this->is_mobile
|
||||
? $analysis->mobile->getBelowTheFoldSections()
|
||||
: $analysis->desktop->getBelowTheFoldSections();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ( $sections as $section ) {
|
||||
if ( $section->height > 0 ) {
|
||||
$results[ $section->className ] = $section->height;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache[ self::SECTIONS ] = $results;
|
||||
|
||||
return $this->cache[ self::SECTIONS ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Asset;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Asset_Loader;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->container->singleton( Asset_Loader::class, Asset_Loader::class );
|
||||
|
||||
add_action(
|
||||
'enqueue_block_editor_assets',
|
||||
$this->container->callback( Asset_Loader::class, 'enqueue_block_editor_scripts' ),
|
||||
20,
|
||||
0
|
||||
);
|
||||
|
||||
add_action(
|
||||
'admin_enqueue_scripts',
|
||||
$this->container->callback( Asset_Loader::class, 'enqueue_post_list_table' )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Nonce\Nonce;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Translation\Text_Repository;
|
||||
|
||||
final class Asset_Loader {
|
||||
|
||||
public const OPTIMIZER_SCRIPT_HANDLE = 'kadence-blocks-admin-optimizer';
|
||||
public const POST_LIST_STYLE_HANDLE = 'kadence-blocks-post-list-table';
|
||||
|
||||
private Asset $asset;
|
||||
private Text_Repository $text_repository;
|
||||
private Nonce $nonce;
|
||||
|
||||
/**
|
||||
* @param Asset $asset
|
||||
* @param Text_Repository $text_repository
|
||||
* @param Nonce $nonce
|
||||
*/
|
||||
public function __construct(
|
||||
Asset $asset,
|
||||
Text_Repository $text_repository,
|
||||
Nonce $nonce
|
||||
) {
|
||||
$this->asset = $asset;
|
||||
$this->text_repository = $text_repository;
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Optimizer scripts when editing a post in Gutenberg.
|
||||
*
|
||||
* @action enqueue_block_editor_assets
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_block_editor_scripts(): void {
|
||||
$this->enqueue_optimizer_js();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Optimizer scripts and the post list table column styles.
|
||||
*
|
||||
* @action admin_enqueue_scripts
|
||||
*
|
||||
* @param string $hook The current admin screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_post_list_table( string $hook ): void {
|
||||
if ( 'edit.php' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_register_style(
|
||||
self::POST_LIST_STYLE_HANDLE,
|
||||
$this->asset->get_url( 'includes/assets/css/post-list-table.min.css' ),
|
||||
[],
|
||||
KADENCE_BLOCKS_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_style( self::POST_LIST_STYLE_HANDLE );
|
||||
|
||||
$this->enqueue_optimizer_js();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Optimizer scripts.
|
||||
*
|
||||
* @action admin_enqueue_scripts
|
||||
* @action enqueue_block_editor_assets
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function enqueue_optimizer_js(): void {
|
||||
$this->asset->enqueue_script( self::OPTIMIZER_SCRIPT_HANDLE, 'dist/kadence-optimizer' );
|
||||
|
||||
// Add post list table optimizer status translations.
|
||||
wp_localize_script(
|
||||
self::OPTIMIZER_SCRIPT_HANDLE,
|
||||
'kbOptimizerL10n',
|
||||
$this->text_repository->all()
|
||||
);
|
||||
|
||||
// TODO: probably move this into a new script.
|
||||
wp_localize_script(
|
||||
self::OPTIMIZER_SCRIPT_HANDLE,
|
||||
'kbOptimizer',
|
||||
[
|
||||
'token' => $this->nonce->create(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Database\Query;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Optimizer_Provider;
|
||||
|
||||
/**
|
||||
* A query builder wrapper for the Optimizer table.
|
||||
*
|
||||
* @see Optimizer_Provider::register_optimizer_query()
|
||||
*/
|
||||
final class Optimizer_Query extends Query {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\Database\Exceptions\DatabaseQueryException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Schema\Tables\Contracts\Table;
|
||||
|
||||
/**
|
||||
* The optimizer database table to store optimizer data.
|
||||
*/
|
||||
final class Optimizer_Table extends Table {
|
||||
|
||||
public const SCHEMA_VERSION = '2.0.5';
|
||||
|
||||
/**
|
||||
* @var string The base table name.
|
||||
*/
|
||||
protected static $base_table_name = 'kb_optimizer';
|
||||
|
||||
/**
|
||||
* @var string The organizational group this table belongs to.
|
||||
*/
|
||||
protected static $group = 'kb';
|
||||
|
||||
/**
|
||||
* @var string|null The slug used to identify the custom table.
|
||||
*/
|
||||
protected static $schema_slug = 'optimizer';
|
||||
|
||||
/**
|
||||
* @var string The field that uniquely identifies a row in the table.
|
||||
*/
|
||||
protected static $uid_column = 'path_hash';
|
||||
|
||||
/**
|
||||
* Overload the update method to first drop the database as this is a temporary table.
|
||||
*
|
||||
* @throws DatabaseQueryException If any of the queries fail.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function update() {
|
||||
try {
|
||||
if ( $this->exists() ) {
|
||||
$this->drop();
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
function_exists( 'error_log' ) && error_log( '[Kadence Blocks]: Unable to drop optimizer table during schema update: ' . $e->getMessage() );
|
||||
return [];
|
||||
}
|
||||
|
||||
return parent::update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function get_definition(): string {
|
||||
global $wpdb;
|
||||
$table_name = self::table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
return "
|
||||
CREATE TABLE `$table_name` (
|
||||
path_hash CHAR(64) PRIMARY KEY COMMENT 'SHA-256 hash of the relative path of the URL used as a unique key for fast lookups',
|
||||
path TEXT NOT NULL COMMENT 'The relative path of the URL, stored for reference and debugging',
|
||||
analysis LONGTEXT NOT NULL COMMENT 'Serialized or JSON-encoded analysis data associated with the path'
|
||||
) {$charset_collate};
|
||||
";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->register_optimizer_query();
|
||||
$this->register_viewport_query();
|
||||
}
|
||||
|
||||
private function register_optimizer_query(): void {
|
||||
$this->container->singleton( Optimizer_Query::class, Optimizer_Query::class );
|
||||
|
||||
$this->container->when( Optimizer_Query::class )
|
||||
->needs( '$table' )
|
||||
->give( static fn(): string => Optimizer_Table::table_name( false ) );
|
||||
}
|
||||
|
||||
private function register_viewport_query(): void {
|
||||
$this->container->singleton( Viewport_Query::class, Viewport_Query::class );
|
||||
|
||||
$this->container->when( Viewport_Query::class )
|
||||
->needs( '$table' )
|
||||
->give( static fn(): string => Viewport_Hash_Table::table_name( false ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Enums\Viewport;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\DB\Database\Exceptions\DatabaseQueryException;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\Schema\Tables\Contracts\Table;
|
||||
|
||||
/**
|
||||
* Database table to store viewport-specific HTML hashes for each URL path.
|
||||
*/
|
||||
final class Viewport_Hash_Table extends Table {
|
||||
|
||||
public const SCHEMA_VERSION = '1.0.5';
|
||||
|
||||
/**
|
||||
* @var string The base table name.
|
||||
*/
|
||||
protected static $base_table_name = 'kb_optimizer_viewport_hashes';
|
||||
|
||||
/**
|
||||
* @var string The organizational group this table belongs to.
|
||||
*/
|
||||
protected static $group = 'kb';
|
||||
|
||||
/**
|
||||
* @var string|null The slug used to identify the custom table.
|
||||
*/
|
||||
protected static $schema_slug = 'viewport_hashes';
|
||||
|
||||
/**
|
||||
* @var string[] The fields that uniquely identify a row in the table (composite key).
|
||||
*/
|
||||
protected static $uid_column = 'path_hash';
|
||||
|
||||
/**
|
||||
* Overload the update method to drop the database first (optional, like your Optimizer_Table).
|
||||
*
|
||||
* @throws DatabaseQueryException If any of the queries fail.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function update() {
|
||||
try {
|
||||
if ( $this->exists() ) {
|
||||
$this->drop();
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
function_exists( 'error_log' ) && error_log( '[Kadence Blocks]: Unable to drop viewport_hash table during schema update: ' . $e->getMessage() );
|
||||
return [];
|
||||
}
|
||||
|
||||
return parent::update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function get_definition(): string {
|
||||
global $wpdb;
|
||||
$table_name = self::table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Build a 'desktop', 'mobile' ENUM input.
|
||||
$enum_values = sprintf( "'%s', '%s'", Viewport::desktop()->value(), Viewport::mobile()->value() );
|
||||
|
||||
return "
|
||||
CREATE TABLE `$table_name` (
|
||||
path_hash CHAR(64) NOT NULL COMMENT 'SHA-256 hash of the path (via \$wp->request), identifies the URL',
|
||||
viewport ENUM($enum_values) NOT NULL COMMENT 'The viewport/device identifier',
|
||||
html_hash VARCHAR(191) NOT NULL COMMENT 'A composite of hashes separated by | in a key\:MD5 value of the HTML markup for this viewport',
|
||||
PRIMARY KEY (path_hash, viewport)
|
||||
) {$charset_collate};
|
||||
";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Database;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Database\Query;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Optimizer_Provider;
|
||||
|
||||
/**
|
||||
* A query builder wrapper for the Viewport_Hash table.
|
||||
*
|
||||
* @see Optimizer_Provider::register_viewport_query()
|
||||
*/
|
||||
final class Viewport_Query extends Query {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Enums;
|
||||
|
||||
/**
|
||||
* A pseudo Viewport enum.
|
||||
*/
|
||||
final class Viewport {
|
||||
|
||||
public const DESKTOP = 'desktop';
|
||||
public const MOBILE = 'mobile';
|
||||
|
||||
/**
|
||||
* The view port.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $value;
|
||||
|
||||
private function __construct( string $value ) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public static function desktop(): self {
|
||||
return new self( self::DESKTOP );
|
||||
}
|
||||
|
||||
public static function mobile(): self {
|
||||
return new self( self::MOBILE );
|
||||
}
|
||||
|
||||
public static function current( bool $is_mobile ): self {
|
||||
return $is_mobile ? self::mobile() : self::desktop();
|
||||
}
|
||||
|
||||
public function value(): string {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals( self $other ): bool {
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Hash;
|
||||
|
||||
final class Background_Processor {
|
||||
|
||||
/**
|
||||
* Try to return the request early so this can be processed in
|
||||
* a background thread.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function try_finish(): void {
|
||||
if ( function_exists( 'fastcgi_finish_request' ) ) {
|
||||
fastcgi_finish_request();
|
||||
} elseif ( function_exists( 'litespeed_finish_request' ) ) {
|
||||
litespeed_finish_request();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Hash;
|
||||
|
||||
/**
|
||||
* Builds a hash based off specific parts of an HTML document
|
||||
* that could affect which optimizations are in place.
|
||||
*
|
||||
* This is a good compromise for URLs that load random data, so we can
|
||||
* keep as many optimizations as possible, even if they may not apply
|
||||
* to some elements.
|
||||
*/
|
||||
final class Hash_Builder {
|
||||
|
||||
/**
|
||||
* Build a composite hash string with individual component hashes.
|
||||
*
|
||||
* @param string $html The full HTML to be returned to the browser.
|
||||
*
|
||||
* @return string The composite hash string.
|
||||
*/
|
||||
public function build_hash( string $html ): string {
|
||||
$components = [
|
||||
'stylesheet' => hash( 'md5', $this->extract_stylesheet_links( $html ) ),
|
||||
'inline' => hash( 'md5', $this->extract_inline_styles( $html ) ),
|
||||
'blocks' => hash( 'md5', $this->extract_block_structure( $html ) ),
|
||||
'structure' => hash( 'md5', $this->extract_structural_elements( $html ) ),
|
||||
];
|
||||
|
||||
return implode(
|
||||
'|',
|
||||
array_map(
|
||||
static fn( string $key, string $hash ): string => "$key:$hash",
|
||||
array_keys( $components ),
|
||||
$components
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two composite hash strings and return which components changed.
|
||||
*
|
||||
* @param string $old_hash The previous composite hash.
|
||||
* @param string $new_hash The new composite hash.
|
||||
*
|
||||
* @return string[] List of component names that changed.
|
||||
*/
|
||||
public function get_changed_components( string $old_hash, string $new_hash ): array {
|
||||
if ( $old_hash === $new_hash ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$old_parts = $this->parse_composite_hash( $old_hash );
|
||||
$new_parts = $this->parse_composite_hash( $new_hash );
|
||||
$changed = [];
|
||||
|
||||
foreach ( $new_parts as $key => $new_value ) {
|
||||
if ( ! isset( $old_parts[ $key ] ) || $old_parts[ $key ] !== $new_value ) {
|
||||
$changed[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a composite hash string into components.
|
||||
*
|
||||
* @param string $composite_hash The composite hash string.
|
||||
*
|
||||
* @return array Associative array of component => hash.
|
||||
*/
|
||||
private function parse_composite_hash( string $composite_hash ): array {
|
||||
$parts = explode( '|', $composite_hash );
|
||||
$result = [];
|
||||
|
||||
foreach ( $parts as $part ) {
|
||||
$split = explode( ':', $part, 2 );
|
||||
|
||||
if ( count( $split ) === 2 ) {
|
||||
$result[ $split[0] ] = $split[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract stylesheet and preload link tags.
|
||||
*
|
||||
* @param string $html The HTML content.
|
||||
*
|
||||
* @return string Concatenated link tags.
|
||||
*/
|
||||
private function extract_stylesheet_links( string $html ): string {
|
||||
preg_match_all( '~<link\b[^>]*\brel=["\'](?:stylesheet|preload)["\'][^>]*>~i', $html, $matches );
|
||||
|
||||
return implode( '', $matches[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract inline style tags.
|
||||
*
|
||||
* @param string $html The HTML content.
|
||||
*
|
||||
* @return string Concatenated style tags.
|
||||
*/
|
||||
private function extract_inline_styles( string $html ): string {
|
||||
preg_match_all( '~<style\b[^>]*>.*?</style>~is', $html, $matches );
|
||||
|
||||
return implode( '', $matches[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract block structures.
|
||||
*
|
||||
* @param string $html The HTML content.
|
||||
*
|
||||
* @return string Pipe-separated block comments.
|
||||
*/
|
||||
private function extract_block_structure( string $html ): string {
|
||||
$matches = [];
|
||||
|
||||
preg_match_all( '~<!-- wp:[^>]*-->~i', $html, $matches );
|
||||
|
||||
return implode( '|', $matches[0] ?? [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structural element counts.
|
||||
*
|
||||
* @param string $html The HTML content.
|
||||
*
|
||||
* @return string Pipe-separated element counts.
|
||||
*/
|
||||
private function extract_structural_elements( string $html ): string {
|
||||
$matches = [];
|
||||
|
||||
preg_match_all( '~<(div|section|article|header|footer|main)[^>]*>~i', $html, $matches );
|
||||
|
||||
$counts = array_count_values( array_map( 'strtolower', $matches[1] ?? [] ) );
|
||||
|
||||
return implode(
|
||||
'|',
|
||||
array_map(
|
||||
static fn( string $element ): string => "$element:" . ( $counts[ $element ] ?? 0 ),
|
||||
[ 'div', 'section', 'article', 'header', 'footer', 'main' ]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Hash;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Enums\Viewport;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path_Factory;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Request\Request;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rule_Collection;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
use KadenceWP\KadenceBlocks\Traits\Viewport_Trait;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Handles output‑buffer hashing during the WordPress shutdown phase to detect
|
||||
* whether the rendered HTML for the current request has changed since the last
|
||||
* optimization pass and invalidates the current optimization data if it's outdated.
|
||||
*/
|
||||
final class Hash_Handler {
|
||||
|
||||
use Viewport_Trait;
|
||||
|
||||
/**
|
||||
* Captures the final HTML before output buffering is
|
||||
* flushed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $html = '';
|
||||
private Hash_Builder $hasher;
|
||||
private Store $store;
|
||||
private Rule_Collection $rules;
|
||||
private Background_Processor $background_processor;
|
||||
private Hash_Store $hash_store;
|
||||
private Path_Factory $path_factory;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @param Hash_Builder $hasher
|
||||
* @param Store $store
|
||||
* @param Rule_Collection $rules
|
||||
* @param Background_Processor $background_processor
|
||||
* @param Hash_Store $hash_store
|
||||
* @param Path_Factory $path_factory
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function __construct(
|
||||
Hash_Builder $hasher,
|
||||
Store $store,
|
||||
Rule_Collection $rules,
|
||||
Background_Processor $background_processor,
|
||||
Hash_Store $hash_store,
|
||||
Path_Factory $path_factory,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->hasher = $hasher;
|
||||
$this->store = $store;
|
||||
$this->rules = $rules;
|
||||
$this->background_processor = $background_processor;
|
||||
$this->hash_store = $hash_store;
|
||||
$this->path_factory = $path_factory;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin buffering the request.
|
||||
*
|
||||
* @action template_redirect
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start_buffering(): void {
|
||||
ob_start( [ $this, 'end_buffering' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the current HTML hash state for the request.
|
||||
*
|
||||
* Compares the freshly generated hash of the final output buffer
|
||||
* against the previously stored hash. If the hash differs, this indicates
|
||||
* that the page content has changed since the last optimization pass.
|
||||
*
|
||||
* This method is intended to be called AS LATE AS POSSIBLE in the `shutdown` phase, after
|
||||
* output buffering has captured the full HTML content.
|
||||
*
|
||||
* If running under fastcgi or litespeed, the request will be returned immediately and the logic
|
||||
* processed in the background.
|
||||
*
|
||||
* @action shutdown
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_hash(): void {
|
||||
if ( SG::get_get_var( Request::QUERY_OPTIMIZER_PREVIEW ) ) {
|
||||
$this->logger->debug( 'Skipping hash check due to optimizer preview query variable.' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->html ) {
|
||||
if ( ! is_admin() ) {
|
||||
$this->logger->debug(
|
||||
'Bypassing Optimizer: No HTML found to check',
|
||||
[
|
||||
'request_uri' => SG::get_server_var( 'REQUEST_URI', 'unknown' ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't check hashes on 404 requests.
|
||||
if ( is_404() ) {
|
||||
$this->logger->debug(
|
||||
'Bypassing Optimizer: 404 not found',
|
||||
[
|
||||
'request_uri' => SG::get_server_var( 'REQUEST_URI', 'unknown' ),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Return request early, if possible, so we can process this in the background.
|
||||
$this->background_processor->try_finish();
|
||||
|
||||
$viewport = Viewport::current( $this->is_mobile() );
|
||||
|
||||
// Process skip rules and bail if required.
|
||||
foreach ( $this->rules->all() as $rule ) {
|
||||
if ( $rule->should_skip() ) {
|
||||
$this->logger->debug(
|
||||
'Bypassing Optimizer: skip rule',
|
||||
[
|
||||
'rule' => get_class( $rule ),
|
||||
'viewport' => $viewport->value(),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->path_factory->make();
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
$this->logger->error(
|
||||
'Hash handler unable to determine the path',
|
||||
[
|
||||
'viewport' => $viewport->value(),
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a hash based on the final HTML markup, note this differs for mobile vs desktop.
|
||||
$hash = $this->hasher->build_hash( $this->html );
|
||||
$stored_hash = $this->hash_store->get( $path, $viewport );
|
||||
|
||||
// The frontend script will pass this get var as a hash set request.
|
||||
$maybe_set_hash = (bool) SG::get_get_var( 'kadence_set_optimizer_hash', false );
|
||||
|
||||
if ( $maybe_set_hash ) {
|
||||
$this->logger->debug(
|
||||
'Attempting to store new optimizer hash',
|
||||
[
|
||||
'path' => $path->path(),
|
||||
'viewport' => $viewport->value(),
|
||||
'hash' => $hash,
|
||||
]
|
||||
);
|
||||
|
||||
// Store the hash for the current viewport.
|
||||
$this->hash_store->set( $path, $viewport, $hash );
|
||||
|
||||
do_action( 'kadence_blocks_optimizer_set_hash', $hash, $path, $viewport );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The HTML has been changed somehow, invalidate the optimization data, so that the next request will not have the data.
|
||||
if ( $stored_hash && $stored_hash !== $hash ) {
|
||||
$changes = $this->hasher->get_changed_components( $stored_hash, $hash );
|
||||
|
||||
$this->logger->debug(
|
||||
'Optimizer hash does not match...deleting',
|
||||
[
|
||||
'path' => $path->path(),
|
||||
'viewport' => $viewport->value(),
|
||||
'changes' => $changes,
|
||||
]
|
||||
);
|
||||
|
||||
// Delete the viewport hash.
|
||||
$this->hash_store->delete( $path, $viewport );
|
||||
|
||||
$analysis = $this->store->get( $path );
|
||||
|
||||
// This page isn't optimized or the data is already invalidated.
|
||||
if ( ! $analysis ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set data to stale to force invalidate data for all viewports.
|
||||
try {
|
||||
$this->logger->debug(
|
||||
'Marking optimizer path as stale to remove on next page load',
|
||||
[
|
||||
'path' => $path->path(),
|
||||
]
|
||||
);
|
||||
|
||||
$analysis->isStale = true;
|
||||
$this->store->set( $path, $analysis );
|
||||
|
||||
do_action( 'kadence_blocks_optimizer_data_invalidated', $analysis->isStale, $path );
|
||||
} catch ( Throwable $e ) {
|
||||
// Our DateTimeImmutable should never throw an exception, but this is here just in case.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->html = '';
|
||||
|
||||
do_action( 'kadence_blocks_hash_check_complete' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML, which will differ as the request proceeds.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html(): string {
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that receives the buffer's contents. Captures the full page HTML
|
||||
* in our property for use when we manage the hash state down the line.
|
||||
*
|
||||
* @param string $html The final HTML.
|
||||
* @param int $phase The bitmask of the PHP_OUTPUT_HANDLER_* constants.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function end_buffering( string $html, int $phase ): string {
|
||||
if ( $phase & PHP_OUTPUT_HANDLER_FINAL ) {
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Hash;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Database\Viewport_Query;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Enums\Viewport;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
|
||||
/**
|
||||
* Stores a hash based on the rendered HTML for each viewport
|
||||
* in order to invalidate optimizer data if it changes.
|
||||
*/
|
||||
final class Hash_Store {
|
||||
|
||||
private Viewport_Query $q;
|
||||
|
||||
public function __construct( Viewport_Query $query ) {
|
||||
$this->q = $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash for a viewport.
|
||||
*
|
||||
* @param Path $path The current path object.
|
||||
* @param Viewport $vp The viewport enum.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get( Path $path, Viewport $vp ): ?string {
|
||||
$hash = $this->q->get_var(
|
||||
$this->q->qb()->select( 'html_hash' )
|
||||
->where( 'path_hash', $path->hash() )
|
||||
->where( 'viewport', $vp->value() )
|
||||
->getSQL()
|
||||
);
|
||||
|
||||
return $hash ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hash for a viewport.
|
||||
*
|
||||
* @param Path $path The current path object.
|
||||
* @param Viewport $vp The viewport enum.
|
||||
* @param string $hash The hash to store.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function set( Path $path, Viewport $vp, string $hash ): bool {
|
||||
return (bool) $this->q->qb()->upsert(
|
||||
[
|
||||
'path_hash' => $path->hash(),
|
||||
'viewport' => $vp->value(),
|
||||
'html_hash' => $hash,
|
||||
],
|
||||
[
|
||||
'path_hash',
|
||||
'viewport',
|
||||
],
|
||||
[
|
||||
'%s',
|
||||
'%s',
|
||||
'%s',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a viewport hash.
|
||||
*
|
||||
* @param Path $path The current path object.
|
||||
* @param Viewport $vp The viewport enum.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( Path $path, Viewport $vp ): bool {
|
||||
return (bool) $this->q->qb()
|
||||
->where( 'path_hash', $path->hash() )
|
||||
->where( 'viewport', $vp->value() )
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Hash;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->register_hash_store();
|
||||
$this->register_hash_handling();
|
||||
}
|
||||
|
||||
private function register_hash_store(): void {
|
||||
$this->container->singleton( Hash_Store::class, Hash_Store::class );
|
||||
}
|
||||
|
||||
private function register_hash_handling(): void {
|
||||
$this->container->singleton( Hash_Builder::class, Hash_Builder::class );
|
||||
$this->container->singleton( Hash_Handler::class, Hash_Handler::class );
|
||||
|
||||
// Don't hook in the optimizer under these scenarios for optimal performance.
|
||||
if (
|
||||
! defined( 'WP_UNINSTALL_PLUGIN' ) &&
|
||||
! wp_installing() &&
|
||||
! wp_doing_ajax() &&
|
||||
! wp_is_json_request() &&
|
||||
! wp_doing_cron()
|
||||
) {
|
||||
add_action(
|
||||
'template_redirect',
|
||||
$this->container->callback( Hash_Handler::class, 'start_buffering' ),
|
||||
1,
|
||||
0
|
||||
);
|
||||
|
||||
add_action(
|
||||
'shutdown',
|
||||
$this->container->callback( Hash_Handler::class, 'check_hash' ),
|
||||
PHP_INT_MAX - 1,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image\Contracts;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\ImageAnalysis;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
/**
|
||||
* Process an img tag to manipulate its markup before the HTML is send back
|
||||
* to the browser.
|
||||
*/
|
||||
interface Processor {
|
||||
|
||||
/**
|
||||
* Manipulates img tags.
|
||||
*
|
||||
* @param WP_HTML_Tag_Processor $p The HTML Tag Processor, set on the current image it's processing.
|
||||
* @param string[] $critical_images The array of critical image src's.
|
||||
* @param array<string, ImageAnalysis> $images The array of all collected images indexed by a unique key.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process( WP_HTML_Tag_Processor $p, array $critical_images, array $images ): void;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Enums\Viewport;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Contracts\Processor;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Traits\Image_Key_Generator_Trait;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path_Factory;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rule_Collection;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
use KadenceWP\KadenceBlocks\Traits\Viewport_Trait;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
/**
|
||||
* Modify HTML img tags before the HTML is sent back to the browser:
|
||||
*
|
||||
* - 1. Set correct lazy load attributes.
|
||||
* - 2. Set the optimal sizes attribute.
|
||||
*/
|
||||
final class Image_Processor {
|
||||
|
||||
use Viewport_Trait;
|
||||
use Image_Key_Generator_Trait;
|
||||
|
||||
private Store $store;
|
||||
private Rule_Collection $rules;
|
||||
private Path_Factory $path_factory;
|
||||
|
||||
/**
|
||||
* The list of image processors.
|
||||
*
|
||||
* @var Processor[]
|
||||
*/
|
||||
private array $processors;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @param Store $store The optimization store.
|
||||
* @param Rule_Collection $rules The rule collection.
|
||||
* @param Path_Factory $path_factory The path factory.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param Processor[] $processors The list of image processors.
|
||||
*/
|
||||
public function __construct(
|
||||
Store $store,
|
||||
Rule_Collection $rules,
|
||||
Path_Factory $path_factory,
|
||||
LoggerInterface $logger,
|
||||
array $processors
|
||||
) {
|
||||
$this->store = $store;
|
||||
$this->rules = $rules;
|
||||
$this->path_factory = $path_factory;
|
||||
$this->logger = $logger;
|
||||
$this->processors = $processors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin buffering the request.
|
||||
*
|
||||
* @action template_redirect
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start_buffering(): void {
|
||||
// Process skip rules and bypass buffering.
|
||||
foreach ( $this->rules->all() as $rule ) {
|
||||
if ( $rule->should_skip() ) {
|
||||
$this->logger->debug(
|
||||
'Skipping Image Processor Buffering: skip rule',
|
||||
[
|
||||
'rule' => get_class( $rule ),
|
||||
'request_uri' => SG::get_server_var( 'REQUEST_URI', 'unknown' ),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ob_start( [ $this, 'stop_buffering' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify each image using our collection of image processors.
|
||||
*
|
||||
* @see self::stop_buffering()
|
||||
*
|
||||
* @param string $html The HTML WordPress will render.
|
||||
* @param Path $path The current path object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function process_images( string $html, Path $path ): string {
|
||||
$analysis = $this->store->get( $path );
|
||||
|
||||
if ( ! $analysis ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$device = $analysis->getDevice( Viewport::current( $this->is_mobile() ) );
|
||||
$critical_images = $device ? $device->criticalImages : [];
|
||||
$images = $analysis->images;
|
||||
|
||||
$images_by_key = [];
|
||||
|
||||
// Reshape the array so we can do O(1) lookups.
|
||||
foreach ( $images as $image ) {
|
||||
if ( isset( $image->src, $image->sizes ) ) {
|
||||
$key = $this->generate_image_key( $image->src, $image->sizes );
|
||||
|
||||
if ( $key !== null ) {
|
||||
$images_by_key[ $key ] = $image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$p = new WP_HTML_Tag_Processor( $html );
|
||||
|
||||
// Add the kb-optimized class to the body.
|
||||
if ( $p->next_tag( 'body' ) ) {
|
||||
$p->add_class( 'kb-optimized' );
|
||||
}
|
||||
|
||||
// Run each image through all image processors.
|
||||
while ( $p->next_tag( 'img' ) ) {
|
||||
$src = $p->get_attribute( 'src' );
|
||||
$classes = $p->get_attribute( 'class' );
|
||||
|
||||
foreach ( $this->processors as $processor ) {
|
||||
|
||||
/**
|
||||
* Allow short-circuiting of processing this image for the
|
||||
* current processor.
|
||||
*
|
||||
* @param bool $should_process Whether we proceed with processing. Defaults to true.
|
||||
* @param string $src The image src.
|
||||
* @param string|null $classes The classes assigned to the img tag.
|
||||
* @param Processor $processor The current processor instance.
|
||||
* @param Path $post The current path object.
|
||||
*/
|
||||
$should_process = apply_filters(
|
||||
'kadence_blocks_optimizer_image_processor',
|
||||
true,
|
||||
$src,
|
||||
$classes,
|
||||
$processor,
|
||||
$path
|
||||
);
|
||||
|
||||
if ( $should_process ) {
|
||||
$processor->process( $p, $critical_images, $images_by_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that receives the buffer's contents.
|
||||
*
|
||||
* @see wp_ob_end_flush_all()
|
||||
* @action shutdown
|
||||
*
|
||||
* @param string $html The HTML WordPress will render.
|
||||
* @param int $phase The bitmask of the PHP_OUTPUT_HANDLER_* constants.
|
||||
*
|
||||
* @return string The modified HTML with lazy loaded and optimal image sizes.
|
||||
*/
|
||||
private function stop_buffering( string $html, int $phase ): string {
|
||||
if ( ! ( $phase & PHP_OUTPUT_HANDLER_FINAL ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->path_factory->make();
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
$this->logger->error(
|
||||
'Failed to get path in image processor buffering',
|
||||
[
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
return $this->process_images( $html, $path );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image\Processors;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Contracts\Processor;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\ImageAnalysis;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
final class Lazy_Load_Processor implements Processor {
|
||||
|
||||
/**
|
||||
* A queue of URLs that are above the fold.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
private ?array $critical_image_queue = null;
|
||||
|
||||
/**
|
||||
* Add or remove the appropriate lazy loading attributes. It's important to note that WordPress
|
||||
* may have already added a loading=lazy attribute to an image that shouldn't have it.
|
||||
*
|
||||
* @param WP_HTML_Tag_Processor $p The HTML Tag Processor, set on the current image it's processing.
|
||||
* @param string[] $critical_images The list of the above the fold image URLs.
|
||||
* @param array<string, ImageAnalysis> $images The array of all collected images indexed by a unique key.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process( WP_HTML_Tag_Processor $p, array $critical_images, array $images ): void {
|
||||
$src = (string) $p->get_attribute( 'src' );
|
||||
|
||||
// Don't lazy load data URLs.
|
||||
if ( str_starts_with( $src, 'data:' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the queue on first run.
|
||||
if ( ! isset( $this->critical_image_queue ) ) {
|
||||
$this->critical_image_queue = $critical_images;
|
||||
}
|
||||
|
||||
// Check if this image is next in our critical queue.
|
||||
if ( $this->critical_image_queue ) {
|
||||
$queue_index = array_search( $src, $this->critical_image_queue, true );
|
||||
|
||||
if ( $queue_index !== false ) {
|
||||
// Remove lazy loading for critical image.
|
||||
if ( 'lazy' === $p->get_attribute( 'loading' ) ) {
|
||||
$p->remove_attribute( 'loading' );
|
||||
}
|
||||
|
||||
// Remove this occurrence from the queue.
|
||||
unset( $this->critical_image_queue[ $queue_index ] );
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Below the fold logic (or after all critical images processed).
|
||||
$p->set_attribute( 'loading', 'lazy' );
|
||||
|
||||
// If WordPress somehow added a high fetch priority, remove it.
|
||||
if ( 'high' === $p->get_attribute( 'fetchpriority' ) ) {
|
||||
$p->remove_attribute( 'fetchpriority' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image\Processors;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Contracts\Processor;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Traits\Image_Key_Generator_Trait;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\ImageAnalysis;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
final class Sizes_Attribute_Processor implements Processor {
|
||||
|
||||
use Image_Key_Generator_Trait;
|
||||
|
||||
/**
|
||||
* Set the optimal sizes attribute for the image.
|
||||
*
|
||||
* @param WP_HTML_Tag_Processor $p The HTML Tag Processor, set on the current image it's processing.
|
||||
* @param string[] $critical_images The array of critical image src's.
|
||||
* @param array<string, ImageAnalysis> $images The array of all collected images indexed by a unique key.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process( WP_HTML_Tag_Processor $p, array $critical_images, array $images ): void {
|
||||
// The analyzer doesn't collect images without a srcset.
|
||||
$srcset = $p->get_attribute( 'srcset' );
|
||||
|
||||
if ( ! $srcset ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$src = $p->get_attribute( 'src' );
|
||||
$sizes = $p->get_attribute( 'sizes' );
|
||||
|
||||
$key = $this->generate_image_key( $src, $sizes );
|
||||
|
||||
if ( null === $key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = $images[ $key ] ?? false;
|
||||
|
||||
if ( ! $image instanceof ImageAnalysis ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the sizes attribute with our optimal sizes.
|
||||
if ( $image->optimalSizes ) {
|
||||
$p->set_attribute( 'sizes', $image->optimalSizes );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Processors\Lazy_Load_Processor;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Image\Processors\Sizes_Attribute_Processor;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->container->singleton( Lazy_Load_Processor::class, Lazy_Load_Processor::class );
|
||||
$this->container->singleton( Image_Processor::class, Image_Processor::class );
|
||||
|
||||
$this->container->when( Image_Processor::class )
|
||||
->needs( '$processors' )
|
||||
->give(
|
||||
fn(): array => [
|
||||
// Add additional image processors here.
|
||||
$this->container->get( Lazy_Load_Processor::class ),
|
||||
$this->container->get( Sizes_Attribute_Processor::class ),
|
||||
]
|
||||
);
|
||||
|
||||
add_action(
|
||||
'template_redirect',
|
||||
$this->container->callback( Image_Processor::class, 'start_buffering' ),
|
||||
1,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Image\Traits;
|
||||
|
||||
trait Image_Key_Generator_Trait {
|
||||
|
||||
/**
|
||||
* Generate a unique key for indexing image data.
|
||||
*
|
||||
* @param string|null|bool $src The image source URL.
|
||||
* @param string|null|bool $sizes The image sizes attribute.
|
||||
*
|
||||
* @return string|null A unique key or null if inputs are missing or non-string.
|
||||
*/
|
||||
private function generate_image_key( $src, $sizes ): ?string {
|
||||
// Only allow actual strings.
|
||||
if ( ! is_string( $src ) || ! is_string( $sizes ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bypass empty attributes.
|
||||
if ( $src === '' || $sizes === '' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return md5( $src . '|' . $sizes );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
/**
|
||||
* Handles setting up the Kadence Row Layout Block for background image
|
||||
* lazy loading.
|
||||
*/
|
||||
final class Background_Lazy_Loader {
|
||||
|
||||
private Asset $asset;
|
||||
private Analysis_Registry $registry;
|
||||
|
||||
public function __construct(
|
||||
Asset $asset,
|
||||
Analysis_Registry $registry
|
||||
) {
|
||||
$this->asset = $asset;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add lazy loading attributes to the row layout block's wrapper div.
|
||||
*
|
||||
* @filter kadence_blocks_row_wrapper_args
|
||||
*
|
||||
* @note Video posters come in as a bgImg as well.
|
||||
*
|
||||
* @param array<string, mixed> $attrs The wrapper div HTML attributes.
|
||||
* @param array<string, mixed> $attributes The current block attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function lazy_load_row_background_images( array $attrs, array $attributes ): array {
|
||||
$bg = $attributes['bgImg'] ?? '';
|
||||
$classes = (string) ( $attrs['class'] ?? '' );
|
||||
|
||||
if ( ! $bg ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if ( ! $this->registry->is_optimized() ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$background_images = $this->registry->get_background_images();
|
||||
|
||||
if ( $background_images ) {
|
||||
$lookup = array_flip( $background_images );
|
||||
|
||||
// Exclude above the fold background images.
|
||||
if ( isset( $lookup[ $bg ] ) ) {
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
|
||||
$is_inline = $attributes['backgroundInline'] ?? false;
|
||||
|
||||
// Add lazy loading data attributes for CSS backgrounds.
|
||||
if ( $classes ) {
|
||||
$attrs['data-kadence-lazy-class'] = $classes;
|
||||
$attrs['data-kadence-lazy-trigger'] = 'viewport';
|
||||
$attrs['data-kadence-lazy-attrs'] = 'class';
|
||||
unset( $attrs['class'] );
|
||||
}
|
||||
|
||||
// Add lazy loading data attributes for inline style background images.
|
||||
if ( $is_inline ) {
|
||||
$attrs['data-kadence-lazy-style'] = $attrs['style'] ?? '';
|
||||
$attrs['data-kadence-lazy-trigger'] ??= 'viewport';
|
||||
|
||||
if ( ! empty( $attrs['data-kadence-lazy-attrs'] ) ) {
|
||||
$attrs['data-kadence-lazy-attrs'] .= ',style';
|
||||
} else {
|
||||
$attrs['data-kadence-lazy-attrs'] = 'style';
|
||||
}
|
||||
|
||||
unset( $attrs['style'] );
|
||||
}
|
||||
|
||||
// Enqueue the lazy loader script if we found a bg.
|
||||
$this->enqueue_scripts();
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load columns with background images.
|
||||
*
|
||||
* @filter kadence_blocks_column_html
|
||||
*
|
||||
* @param string $html The kadence/column html.
|
||||
* @param array $attributes The block attributes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lazy_load_column_backgrounds( string $html, array $attributes ): string {
|
||||
$bg = $attributes['backgroundImg'][0]['bgImg'] ?? false;
|
||||
|
||||
if ( ! $bg ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
if ( ! $this->registry->is_optimized() ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$background_images = $this->registry->get_background_images();
|
||||
|
||||
if ( $background_images ) {
|
||||
$lookup = array_flip( $background_images );
|
||||
|
||||
// Exclude above the fold background images.
|
||||
if ( isset( $lookup[ $bg ] ) ) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
$p = new WP_HTML_Tag_Processor( $html );
|
||||
|
||||
if ( ! $p->next_tag( [ 'class_name' => 'wp-block-kadence-column' ] ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$class_attr = $p->get_attribute( 'class' );
|
||||
|
||||
if ( ! $class_attr ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$p->set_attribute( 'data-kadence-lazy-class', $class_attr );
|
||||
$p->set_attribute( 'data-kadence-lazy-trigger', 'viewport' );
|
||||
$p->set_attribute( 'data-kadence-lazy-attrs', 'class' );
|
||||
|
||||
$p->remove_attribute( 'class' );
|
||||
|
||||
// Enqueue the lazy loader script if we found a bg.
|
||||
$this->enqueue_scripts();
|
||||
|
||||
return $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the lazy loading frontend script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function enqueue_scripts(): void {
|
||||
$this->asset->enqueue_script( 'lazy-loader', 'dist/lazy-loader' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Lazy_Load\Sections\Lazy_Render_Decider;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
/**
|
||||
* Process section Optimization data to set content visibility on the Kadence row layout and column
|
||||
* block.
|
||||
*
|
||||
* @phpstan-type HtmlClassHeightMap array<string, float> Class attribute => height in pixels.
|
||||
*/
|
||||
final class Element_Lazy_Loader {
|
||||
|
||||
private const CONTENT_VISIBILITY = 'content-visibility';
|
||||
private const CONTAIN_INTRINSIC_SIZE = 'contain-intrinsic-size';
|
||||
|
||||
private Lazy_Render_Decider $decider;
|
||||
|
||||
public function __construct(
|
||||
Lazy_Render_Decider $decider
|
||||
) {
|
||||
$this->decider = $decider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content-visibility CSS to below the fold Kadence row elements.
|
||||
*
|
||||
* @filter kadence_blocks_row_wrapper_args
|
||||
*
|
||||
* @param array<string, mixed> $args The wrapper div HTML attributes.
|
||||
* @param array<string, mixed> $attributes The current block attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function set_content_visibility_for_row( array $args, array $attributes ): array {
|
||||
$unique_id = $attributes['uniqueID'] ?? false;
|
||||
|
||||
if ( ! $unique_id ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$classes = (string) ( $args['class'] ?? '' );
|
||||
|
||||
// Explode class string into array for exact matching.
|
||||
$class_list = array_filter( preg_split( '/\s+/', $classes ) );
|
||||
|
||||
// Bypass lazy rendering if we find an excluded class (exact match only).
|
||||
if ( ! $this->decider->should_lazy_render( $class_list ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$height = $this->decider->get_section_height_by_unique_id( $unique_id );
|
||||
|
||||
if ( $height <= 0 ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$current_style = (string) ( $args['style'] ?? '' );
|
||||
|
||||
if ( $this->has_content_visibility( $current_style ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$args['style'] = $this->prepend_style( $height, $current_style );
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content visibility on found kadence/column sections.
|
||||
*
|
||||
* @filter kadence_blocks_column_html
|
||||
*
|
||||
* @param string $html The kadence/column html.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function modify_column_html( string $html ): string {
|
||||
$p = new WP_HTML_Tag_Processor( $html );
|
||||
|
||||
if ( ! $p->next_tag( [ 'class_name' => 'wp-block-kadence-column' ] ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$class_attr = $p->get_attribute( 'class' );
|
||||
|
||||
if ( ! $class_attr ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$height = $this->decider->get_section_height_by_class_attr( $class_attr );
|
||||
|
||||
if ( $height <= 0 ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$current_style = (string) $p->get_attribute( 'style' );
|
||||
|
||||
if ( $this->has_content_visibility( $current_style ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$p->set_attribute( 'style', $this->prepend_style( $height, $current_style ) );
|
||||
|
||||
return $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a style attribute already has content-visibility.
|
||||
*
|
||||
* @param string $current_style The current style attribute value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_content_visibility( string $current_style ): bool {
|
||||
return str_contains( $current_style, self::CONTENT_VISIBILITY ) ||
|
||||
str_contains( $current_style, self::CONTAIN_INTRINSIC_SIZE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend the content-visibility styles to an existing style string.
|
||||
*
|
||||
* @param float $height The section height.
|
||||
* @param string $current_style The current style.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function prepend_style( float $height, string $current_style ): string {
|
||||
return sprintf(
|
||||
'%s: auto;%s: auto %dpx;%s',
|
||||
self::CONTENT_VISIBILITY,
|
||||
self::CONTAIN_INTRINSIC_SIZE,
|
||||
$height,
|
||||
trim( $current_style )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Lazy_Load\Sections\Lazy_Render_Decider;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Request\Request;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
use KadenceWP\KadenceBlocks\Traits\Viewport_Trait;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
use Viewport_Trait;
|
||||
|
||||
public function register(): void {
|
||||
$this->register_analysis_registry();
|
||||
$this->register_element_lazy_loader();
|
||||
$this->register_slider_lazy_loader();
|
||||
$this->register_video_poster_lazy_loader();
|
||||
$this->register_background_lazy_loader();
|
||||
}
|
||||
|
||||
private function register_analysis_registry(): void {
|
||||
$this->container->singleton( Analysis_Registry::class, Analysis_Registry::class );
|
||||
|
||||
$this->container->when( Analysis_Registry::class )
|
||||
->needs( '$is_mobile' )
|
||||
->give( fn(): bool => $this->is_mobile() );
|
||||
}
|
||||
|
||||
private function register_element_lazy_loader(): void {
|
||||
$this->container->singleton( Lazy_Render_Decider::class, Lazy_Render_Decider::class );
|
||||
$this->container->singleton( Element_Lazy_Loader::class, Element_Lazy_Loader::class );
|
||||
|
||||
/**
|
||||
* Filters the list of CSS classes that should be excluded from lazy loading for
|
||||
* section elements.
|
||||
*
|
||||
* @param string[] $excluded_classes An array of CSS class names. If a row has one
|
||||
* of these classes, it will be excluded from lazy loading.
|
||||
*/
|
||||
$excluded_classes = apply_filters(
|
||||
'kadence_blocks_optimizer_section_lazy_load_excluded_classes',
|
||||
[
|
||||
'kt-jarallax',
|
||||
]
|
||||
);
|
||||
|
||||
$this->container->when( Lazy_Render_Decider::class )
|
||||
->needs( '$excluded_classes' )
|
||||
->give( static fn(): array => $excluded_classes );
|
||||
|
||||
// Do not perform element lazy loading on optimizer requests.
|
||||
if ( $this->container->get( Request::class )->is_optimizer_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_row_wrapper_args',
|
||||
$this->container->callback( Element_Lazy_Loader::class, 'set_content_visibility_for_row' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_column_html',
|
||||
$this->container->callback( Element_Lazy_Loader::class, 'modify_column_html' ),
|
||||
10,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
private function register_slider_lazy_loader(): void {
|
||||
$this->container->singleton( Slider_Lazy_Loader::class, Slider_Lazy_Loader::class );
|
||||
|
||||
// Do not perform slider lazy loading on optimizer requests.
|
||||
if ( $this->container->get( Request::class )->is_optimizer_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_row_slider_attrs',
|
||||
$this->container->callback( Slider_Lazy_Loader::class, 'lazy_load_row_slider' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
private function register_video_poster_lazy_loader(): void {
|
||||
$this->container->singleton( Video_Poster_Lazy_Loader::class, Video_Poster_Lazy_Loader::class );
|
||||
|
||||
// Do not perform slider lazy loading on optimizer requests.
|
||||
if ( $this->container->get( Request::class )->is_optimizer_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_row_video_attrs',
|
||||
$this->container->callback( Video_Poster_Lazy_Loader::class, 'lazy_load_row_video_poster' ),
|
||||
10,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
private function register_background_lazy_loader(): void {
|
||||
$this->container->singleton( Background_Lazy_Loader::class, Background_Lazy_Loader::class );
|
||||
|
||||
// Do not perform background lazy loading on optimizer requests.
|
||||
if ( $this->container->get( Request::class )->is_optimizer_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_row_wrapper_args',
|
||||
$this->container->callback( Background_Lazy_Loader::class, 'lazy_load_row_background_images' ),
|
||||
20,
|
||||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'kadence_blocks_column_html',
|
||||
$this->container->callback( Background_Lazy_Loader::class, 'lazy_load_column_backgrounds' ),
|
||||
20,
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load\Sections;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
|
||||
/**
|
||||
* Decide whether we should lazy render a section or not.
|
||||
*/
|
||||
class Lazy_Render_Decider {
|
||||
|
||||
private Analysis_Registry $registry;
|
||||
|
||||
/**
|
||||
* A list of CSS classes that will bypass lazy rendering.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $excluded_classes;
|
||||
|
||||
public function __construct(
|
||||
Analysis_Registry $registry,
|
||||
array $excluded_classes
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->excluded_classes = $excluded_classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should lazy load a section.
|
||||
*
|
||||
* @param string[] $class_list The class of classes to check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_lazy_render( array $class_list ): bool {
|
||||
foreach ( $this->excluded_classes as $excluded_class ) {
|
||||
if ( in_array( $excluded_class, $class_list, true ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section's height by a Kadence unique ID.
|
||||
*
|
||||
* @param string $unique_id The unique ID, e.g. 1722_f1b7a6-ea.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_section_height_by_unique_id( string $unique_id ): float {
|
||||
foreach ( $this->registry->get_valid_sections() as $class_attr => $height ) {
|
||||
if ( str_contains( $class_attr, $unique_id ) ) {
|
||||
return $height;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section's height by matching the complete class attribute value.
|
||||
*
|
||||
* @param string $class_attr The class attribute, e.g. `kb-row-layout-wrap kb-row-layout-id1722_ab152a-75 alignfull`.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_section_height_by_class_attr( string $class_attr ): float {
|
||||
return $this->registry->get_valid_sections()[ $class_attr ] ?? 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
|
||||
final class Slider_Lazy_Loader {
|
||||
|
||||
private Analysis_Registry $registry;
|
||||
|
||||
public function __construct( Analysis_Registry $registry ) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load the row slider background image.
|
||||
*
|
||||
* @filter kadence_blocks_row_slider_attrs
|
||||
*
|
||||
* @param array<string, mixed> $attrs The HTML attributes.
|
||||
* @param array<string, mixed> $attributes The row block's attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function lazy_load_row_slider( array $attrs, array $attributes ): array {
|
||||
if ( ! $this->registry->is_optimized() ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$sliders = $attributes['backgroundSlider'] ?? [];
|
||||
|
||||
// Exclude sliders with above the fold background images.
|
||||
if ( $sliders ) {
|
||||
$background_images = $this->registry->get_background_images();
|
||||
|
||||
if ( $background_images ) {
|
||||
$lookup = array_flip( $background_images );
|
||||
|
||||
foreach ( $sliders as $slide ) {
|
||||
$bg = $slide['bgImg'] ?? '';
|
||||
|
||||
if ( $bg && isset( $lookup[ $bg ] ) ) {
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$attrs['class'] = trim( ( $attrs['class'] ?? '' ) . ' kb-lazy-bg-pending' );
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Lazy_Load;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
|
||||
final class Video_Poster_Lazy_Loader {
|
||||
|
||||
private Asset $asset;
|
||||
private Analysis_Registry $registry;
|
||||
|
||||
public function __construct(
|
||||
Asset $asset,
|
||||
Analysis_Registry $registry
|
||||
) {
|
||||
$this->asset = $asset;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load video posters.
|
||||
*
|
||||
* @example `<video poster="https://wordpress.test/some-image.jpg">`
|
||||
*
|
||||
* @filter kadence_blocks_row_video_attrs
|
||||
*
|
||||
* @param array<string, mixed> $attrs The HTML attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function lazy_load_row_video_poster( array $attrs ): array {
|
||||
$poster = $attrs['poster'] ?? '';
|
||||
|
||||
if ( ! $poster ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if ( ! $this->registry->is_optimized() ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$critical_images = $this->registry->get_background_images();
|
||||
$lookup = array_flip( $critical_images );
|
||||
|
||||
// Bypass above the fold images.
|
||||
if ( isset( $lookup[ $poster ] ) ) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$attrs['data-kadence-lazy-poster'] = $poster;
|
||||
$attrs['data-kadence-lazy-trigger'] = 'viewport';
|
||||
$attrs['data-kadence-lazy-attrs'] = 'poster';
|
||||
|
||||
unset( $attrs['poster'] );
|
||||
|
||||
// Enqueue the frontend lazy loader script.
|
||||
$this->asset->enqueue_script( 'lazy-loader', 'dist/lazy-loader' );
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createLazyLoader } from './lazy-loader';
|
||||
|
||||
const lazyLoader = createLazyLoader();
|
||||
lazyLoader.observeElements();
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Use the IntersectionObserver to replace our lazy load attributes with the originals
|
||||
* when the element comes into the viewport.
|
||||
*
|
||||
* @param {IntersectionObserverInit} options The IntersectionObserver options.
|
||||
*/
|
||||
export const createLazyLoader = (options = { rootMargin: '300px 0px' }) => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (e.isIntersecting) {
|
||||
observer.unobserve(e.target);
|
||||
|
||||
// Get the attributes to replace: data-kadence-lazy-attrs.
|
||||
const lazyAttributes = e.target.dataset.kadenceLazyAttrs;
|
||||
|
||||
// Merge or copy the attribute values back to their original attributes.
|
||||
if (lazyAttributes) {
|
||||
lazyAttributes.split(',').forEach((attributeName) => {
|
||||
const lazyValue = e.target.getAttribute(`data-kadence-lazy-${attributeName}`);
|
||||
|
||||
if (lazyValue) {
|
||||
const existingValue = e.target.getAttribute(attributeName);
|
||||
|
||||
if (attributeName === 'class') {
|
||||
// Merge class names.
|
||||
e.target.classList.add(...lazyValue.trim().split(/\s+/));
|
||||
} else {
|
||||
// Merge or set values if the attribute already exists.
|
||||
const mergedValue = existingValue
|
||||
? `${existingValue.trim()} ${lazyValue}`.trim()
|
||||
: lazyValue;
|
||||
|
||||
e.target.setAttribute(attributeName, mergedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the lazy data attributes.
|
||||
[...e.target.attributes]
|
||||
.filter((attr) => attr.name.startsWith('data-kadence-lazy'))
|
||||
.forEach((attr) => e.target.removeAttribute(attr.name));
|
||||
|
||||
// Fire an event so other JS scripts can hook into this and re-run their initialization.
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('kadence-lazy-loaded', {
|
||||
detail: { element: e.target },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, options);
|
||||
|
||||
const observeElements = (selector = "[data-kadence-lazy-trigger='viewport']") => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((e) => observer.observe(e));
|
||||
};
|
||||
|
||||
return { observeElements };
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Nonce;
|
||||
|
||||
/**
|
||||
* Custom nonce implementation for when the optimizer is running.
|
||||
*/
|
||||
final class Nonce {
|
||||
|
||||
/**
|
||||
* Random negative ID to never clash with a possible existing
|
||||
* WordPress user ID.
|
||||
*/
|
||||
public const NONCE_ID = -1223685899;
|
||||
|
||||
private string $action;
|
||||
|
||||
/**
|
||||
* @param string $action The nonce action for the optimizer.
|
||||
*/
|
||||
public function __construct( string $action = 'kb-optimizer' ) {
|
||||
$this->action = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a nonce for use for a logged-out user, even if the user is logged in.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function create(): string {
|
||||
$uid = get_current_user_id();
|
||||
wp_set_current_user( 0 );
|
||||
$nonce = wp_create_nonce( $this->action );
|
||||
wp_set_current_user( $uid );
|
||||
|
||||
return $nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the nonce as a logged-out user and then restore the original user.
|
||||
*
|
||||
* @param string $nonce The nonce value.
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function verify( string $nonce ) {
|
||||
$uid = get_current_user_id();
|
||||
wp_set_current_user( 0 );
|
||||
$verified = wp_verify_nonce( $nonce, $this->action );
|
||||
wp_set_current_user( $uid );
|
||||
|
||||
return $verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the nonce ID in order to generate a unique nonce value for just this action.
|
||||
*
|
||||
* This way, it's not shared with all logged-out users.
|
||||
*
|
||||
* @filter nonce_user_logged_out
|
||||
*
|
||||
* @param int $uid The current user ID.
|
||||
* @param string|int $action The nonce action.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function customize_nonce_id( int $uid, $action ): int {
|
||||
if ( 0 !== $uid || $action !== $this->action ) {
|
||||
return $uid;
|
||||
}
|
||||
|
||||
return self::NONCE_ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Nonce;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->container->singleton( Nonce::class, Nonce::class );
|
||||
|
||||
add_filter(
|
||||
'nonce_user_logged_out',
|
||||
$this->container->callback( Nonce::class, 'customize_nonce_id' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals;
|
||||
|
||||
final class Optimizer_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* List of Optimizer providers to register, in order.
|
||||
*
|
||||
* @var class-string<Provider>[]
|
||||
*/
|
||||
private const PROVIDERS = [
|
||||
Database\Provider::class,
|
||||
Translation\Provider::class,
|
||||
Skip_Rules\Provider::class,
|
||||
Hash\Provider::class,
|
||||
Nonce\Provider::class,
|
||||
Request\Provider::class,
|
||||
Store\Provider::class,
|
||||
Status\Provider::class,
|
||||
Asset\Provider::class,
|
||||
Post_List_Table\Provider::class,
|
||||
Rest\Provider::class,
|
||||
Lazy_Load\Provider::class,
|
||||
Image\Provider::class,
|
||||
Resource_Hints\Provider::class,
|
||||
];
|
||||
|
||||
public function register(): void {
|
||||
$this->container->singleton( State::class, State::class );
|
||||
|
||||
/**
|
||||
* Filter the optimizer enabled state.
|
||||
*
|
||||
* @param bool $enabled Whether the optimizer is enabled or not.
|
||||
*/
|
||||
$enabled = (bool) apply_filters(
|
||||
'kadence_blocks_optimizer_enabled',
|
||||
$this->container->get( State::class )->enabled()
|
||||
);
|
||||
|
||||
if ( ! $enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->register_mobile_override();
|
||||
|
||||
foreach ( self::PROVIDERS as $provider ) {
|
||||
$this->container->register( $provider );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow force overriding wp_is_mobile with a query string variable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_mobile_override(): void {
|
||||
add_filter(
|
||||
'wp_is_mobile',
|
||||
static function ( bool $is_mobile ): bool {
|
||||
if ( SuperGlobals::get_get_var( 'kadence_is_mobile' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $is_mobile;
|
||||
},
|
||||
10,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Path;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The Path Data Transfer Object.
|
||||
*
|
||||
* Represents a URL path and optionally its associated post ID.
|
||||
* The post_id is optional but recommended for reliable status synchronization.
|
||||
*
|
||||
* @see Path_Factory
|
||||
*/
|
||||
final class Path {
|
||||
|
||||
/**
|
||||
* The path from $wp->request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $path;
|
||||
|
||||
/**
|
||||
* Optional post ID associated with this path.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private ?int $post_id;
|
||||
|
||||
/**
|
||||
* @param string $path The path from $wp->request.
|
||||
* @param int|null $post_id Optional post ID for status synchronization.
|
||||
*
|
||||
* @throws InvalidArgumentException If the path is empty.
|
||||
*/
|
||||
public function __construct( string $path, ?int $post_id = null ) {
|
||||
if ( ! $path ) {
|
||||
throw new InvalidArgumentException( 'Cannot hash an empty path. Verify you are using this after the wp hook fired.' );
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->post_id = $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function path(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sha256 hash for the path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hash(): string {
|
||||
return hash( 'sha256', $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated post ID if available.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function post_id(): ?int {
|
||||
return $this->post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Path with post ID attached (immutable).
|
||||
*
|
||||
* Useful for adding post context to an existing Path object.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function with_post_id( int $post_id ): self {
|
||||
return new self( $this->path, $post_id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Path;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* A path object to generate a unique key for each URL on a site.
|
||||
*/
|
||||
final class Path_Factory {
|
||||
|
||||
/**
|
||||
* Create a path object based on the current relative path and $post global.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the path is empty.
|
||||
*
|
||||
* @return Path
|
||||
*/
|
||||
public function make(): Path {
|
||||
$uri = SG::get_server_var( 'REQUEST_URI', '' );
|
||||
|
||||
// Normalize leading slashes BEFORE wp_parse_url to prevent // being treated as protocol-relative URL.
|
||||
$uri = preg_replace( '#^/+#', '/', $uri );
|
||||
|
||||
if ( $uri === null ) {
|
||||
return new Path( '' );
|
||||
}
|
||||
|
||||
$path = wp_parse_url( $uri, PHP_URL_PATH ) ?: '';
|
||||
|
||||
// Normalize multiple slashes within the path.
|
||||
$normalized = preg_replace( '#/{2,}#', '/', $path );
|
||||
|
||||
if ( $normalized === null || $normalized === '' ) {
|
||||
return new Path( '' );
|
||||
}
|
||||
|
||||
// Ensure the path starts with a slash.
|
||||
if ( ! str_starts_with( $normalized, '/' ) ) {
|
||||
$normalized = '/' . $normalized;
|
||||
}
|
||||
|
||||
// This will get the proper front/posts page ID.
|
||||
$current_post = get_queried_object();
|
||||
|
||||
if ( ! $current_post instanceof WP_Post ) {
|
||||
return new Path( $normalized, null );
|
||||
}
|
||||
|
||||
return new Path( $normalized, $current_post->ID );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
/**
|
||||
* Adds actions to the Post List Table's "bulk actions" dropdown.
|
||||
*/
|
||||
final class Bulk_Action_Manager {
|
||||
|
||||
public const OPTIMIZE_POSTS = 'kb_optimize_posts';
|
||||
public const REMOVE_OPTIMIZATION = 'kb_optimize_remove';
|
||||
|
||||
/**
|
||||
* Add bulk actions to the post list table. These are then handled by the frontend js
|
||||
* under Optimizer/assets/js/bulk-actions/index.js
|
||||
*
|
||||
* @filter bulk_actions-edit-post
|
||||
* @filter bulk_actions-edit-page
|
||||
*
|
||||
* @param array<string, string|array<string, string>> $actions The existing actions.
|
||||
*
|
||||
* @return array<string, string|array<string, string>>
|
||||
*/
|
||||
public function register_actions( array $actions ): array {
|
||||
$actions[ __( 'KB Optimizer', 'kadence-blocks' ) ] = [
|
||||
self::OPTIMIZE_POSTS => __( 'Optimize', 'kadence-blocks' ),
|
||||
self::REMOVE_OPTIMIZATION => __( 'Remove Optimization', 'kadence-blocks' ),
|
||||
];
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a column in a post list table.
|
||||
*/
|
||||
final class Column {
|
||||
|
||||
public string $slug;
|
||||
public string $label;
|
||||
public string $meta_key;
|
||||
|
||||
/**
|
||||
* @param string $slug The column slug.
|
||||
* @param string $label The i18n friendly label.
|
||||
* @param string $meta_key The meta key to sort by.
|
||||
*
|
||||
* @throws InvalidArgumentException If slug or label are empty or whitespace-only.
|
||||
*/
|
||||
public function __construct(
|
||||
string $slug,
|
||||
string $label,
|
||||
string $meta_key
|
||||
) {
|
||||
if ( empty( trim( $slug ) ) ) {
|
||||
throw new InvalidArgumentException( 'Column slug cannot be empty.' );
|
||||
}
|
||||
|
||||
if ( empty( trim( $label ) ) ) {
|
||||
throw new InvalidArgumentException( 'Column label cannot be empty.' );
|
||||
}
|
||||
|
||||
$this->slug = $slug;
|
||||
$this->label = $label;
|
||||
$this->meta_key = $meta_key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
/**
|
||||
* Manages WordPress hooks for post list table columns.
|
||||
*/
|
||||
final class Column_Hook_Manager {
|
||||
|
||||
/**
|
||||
* @var Column_Registrar[]
|
||||
*/
|
||||
private array $columns;
|
||||
|
||||
/**
|
||||
* @param Column_Registrar[] $columns
|
||||
*/
|
||||
public function __construct( array $columns ) {
|
||||
$this->columns = $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks for all columns on applicable post types.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
$post_types = $this->get_applicable_post_types();
|
||||
|
||||
foreach ( $this->columns as $column ) {
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$this->register_column_hooks( $column, $post_type );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post types that should have optimizer columns.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function get_applicable_post_types(): array {
|
||||
$post_types = get_post_types(
|
||||
[
|
||||
'show_ui' => true,
|
||||
],
|
||||
);
|
||||
|
||||
$post_types = array_filter( $post_types, static fn( $pt ): bool => is_post_type_viewable( $pt ) );
|
||||
|
||||
/**
|
||||
* @param string[] $excluded_post_types Post type names to exclude.
|
||||
*/
|
||||
$excluded = apply_filters(
|
||||
'kadence_blocks_excluded_optimizer_post_types',
|
||||
[
|
||||
'attachment',
|
||||
'advanced_page',
|
||||
'kadence_element',
|
||||
'kt_reviews',
|
||||
'kt_size_chart',
|
||||
]
|
||||
);
|
||||
|
||||
return array_diff( $post_types, $excluded );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks for a specific column and post type.
|
||||
*
|
||||
* @param Column_Registrar $column The column to register.
|
||||
* @param string $post_type The post type name this will be applied to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_column_hooks( Column_Registrar $column, string $post_type ): void {
|
||||
add_filter(
|
||||
"manage_{$post_type}_posts_columns",
|
||||
[ $column, 'add_header' ]
|
||||
);
|
||||
|
||||
add_action(
|
||||
"manage_{$post_type}_posts_custom_column",
|
||||
[ $column, 'render' ],
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
"manage_edit-{$post_type}_sortable_columns",
|
||||
[ $column, 'mark_sortable' ]
|
||||
);
|
||||
|
||||
add_action(
|
||||
'pre_get_posts',
|
||||
[ $column, 'sort' ],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts\Renderable;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts\Sort_Strategy;
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* Registers Post List Table columns.
|
||||
*/
|
||||
final class Column_Registrar {
|
||||
|
||||
private Column $column;
|
||||
private Renderable $renderable;
|
||||
private ?Sort_Strategy $sort_strategy;
|
||||
private bool $is_sortable;
|
||||
|
||||
public function __construct(
|
||||
Column $column,
|
||||
Renderable $renderable,
|
||||
?Sort_Strategy $sort_strategy = null,
|
||||
bool $is_sortable = false
|
||||
) {
|
||||
$this->column = $column;
|
||||
$this->renderable = $renderable;
|
||||
$this->sort_strategy = $sort_strategy;
|
||||
$this->is_sortable = $is_sortable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying column object.
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public function column(): Column {
|
||||
return $this->column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the column header to the table.
|
||||
*
|
||||
* @filter manage_{$post_type}_posts_columns
|
||||
*
|
||||
* @param string[] $columns The existing columns.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function add_header( array $columns ): array {
|
||||
$columns[ $this->column->slug ] = $this->column->label;
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the column as sortable.
|
||||
*
|
||||
* @filter manage_{$this->screen->id}_sortable_columns
|
||||
*
|
||||
* @param array $columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function mark_sortable( array $columns ): array {
|
||||
if ( ! $this->is_sortable ) {
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/* translators: %s: The column label */
|
||||
$ordered_by = sprintf( __( 'Table ordered by %s', 'kadence-blocks' ), $this->column->label );
|
||||
|
||||
$columns[ $this->column->slug ] = [
|
||||
$this->column->slug,
|
||||
true,
|
||||
$this->column->label,
|
||||
$ordered_by,
|
||||
'asc',
|
||||
];
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to sort the column by its strategy.
|
||||
*
|
||||
* @action pre_get_posts
|
||||
*
|
||||
* @param WP_Query $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sort( WP_Query $query ): void {
|
||||
if ( ! $this->is_sortable || ! isset( $this->sort_strategy ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sort_strategy->sort( $query, $this->column );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the column value.
|
||||
*
|
||||
* @filter manage_{$post->post_type}_posts_custom_column
|
||||
*
|
||||
* @param string $slug
|
||||
* @param int $post_id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render( string $slug, int $post_id ): void {
|
||||
if ( $slug !== $this->column->slug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->renderable->render( $post_id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts;
|
||||
|
||||
interface Renderable {
|
||||
|
||||
/**
|
||||
* Echo a column value in a post list table.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render( int $post_id ): void;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Column;
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* The sort strategy to use for when a column is sorted.
|
||||
*/
|
||||
interface Sort_Strategy {
|
||||
|
||||
/**
|
||||
* Modify WP_Query to sort by this column.
|
||||
*
|
||||
* @action pre_get_posts
|
||||
*
|
||||
* @param WP_Query $query
|
||||
* @param Column $column
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sort( WP_Query $query, Column $column ): void;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts;
|
||||
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* Sort a Post List Table by a custom table.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface Table_Sorter {
|
||||
|
||||
/**
|
||||
* Reorder the posts for the Post List Table before they returned.
|
||||
*
|
||||
* @filter the_posts
|
||||
*
|
||||
* @param string $order The sort order: ASC|DESC.
|
||||
* @param WP_Post[] $posts The posts being listed.
|
||||
*
|
||||
* @return WP_Post[] The sorted posts, if applicable.
|
||||
*/
|
||||
public function sort( string $order, array $posts ): array;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Meta;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Status;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals;
|
||||
|
||||
/**
|
||||
* Post List Table filter.
|
||||
*/
|
||||
final class Filter {
|
||||
|
||||
public const STATUS = 'kb_optimizer_status';
|
||||
|
||||
/**
|
||||
* Add a filter dropdown to the Post List Table.
|
||||
*
|
||||
* @action restrict_manage_posts
|
||||
*
|
||||
* @param string $post_type The current post type being viewed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_filter( string $post_type ): void {
|
||||
if ( ! is_post_type_viewable( $post_type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current = SuperGlobals::get_get_var( self::STATUS, '' );
|
||||
?>
|
||||
<label for="kb-optimizer-filter" class="screen-reader-text"><?php echo esc_html__( 'Filter by Optimizer Status', 'kadence-blocks' ); ?></label>
|
||||
<select name="<?php echo esc_attr( self::STATUS ); ?>" id="kb-optimizer-filter">
|
||||
<option value="">
|
||||
<?php echo esc_html__( 'All Optimizer Statuses', 'kadence-blocks' ); ?>
|
||||
</option>
|
||||
<option
|
||||
value="<?php echo esc_attr( Status::STALE ); ?>"
|
||||
<?php selected( $current, Status::STALE ); ?>>
|
||||
<?php echo esc_html__( 'Outdated', 'kadence-blocks' ); ?>
|
||||
</option>
|
||||
<option
|
||||
value="<?php echo esc_attr( Status::EXCLUDED ); ?>"
|
||||
<?php selected( $current, Status::EXCLUDED ); ?>>
|
||||
<?php echo esc_html__( 'Excluded', 'kadence-blocks' ); ?>
|
||||
</option>
|
||||
<option
|
||||
value="<?php echo esc_attr( Status::OPTIMIZED ); ?>"
|
||||
<?php selected( $current, Status::OPTIMIZED ); ?>>
|
||||
<?php echo esc_html__( 'Optimized', 'kadence-blocks' ); ?>
|
||||
</option>
|
||||
<option
|
||||
value="<?php echo esc_attr( Status::NOT_OPTIMIZED ); ?>"
|
||||
<?php selected( $current, Status::NOT_OPTIMIZED ); ?>>
|
||||
<?php echo esc_html__( 'Not Optimized', 'kadence-blocks' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter posts on the Post List Table by their optimization status.
|
||||
*
|
||||
* @action pre_get_posts
|
||||
*
|
||||
* @param \WP_Query $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function filter_posts( \WP_Query $query ) {
|
||||
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = SuperGlobals::get_get_var( self::STATUS );
|
||||
|
||||
if ( $status === null || $status === '' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter by everything besides "not optimized".
|
||||
$meta_query = [
|
||||
[
|
||||
'key' => Meta::KEY,
|
||||
'compare' => '=',
|
||||
'value' => $status,
|
||||
'type' => 'NUMERIC',
|
||||
],
|
||||
];
|
||||
|
||||
if ( Status::NOT_OPTIMIZED === (int) $status ) {
|
||||
$meta_query = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => Meta::KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => Meta::KEY,
|
||||
'compare' => '=',
|
||||
'value' => $status,
|
||||
'type' => 'NUMERIC',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$query->set( 'meta_query', $meta_query );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Renderers\Optimizer_Renderer;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Meta;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Cached_Store_Decorator;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Table_Store;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public const OPTIMIZER_COLUMN = 'kadence_blocks.optimizer.optimizer_column';
|
||||
public const OPTIMIZER_COLUMN_REGISTRAR = 'kadence_blocks.optimizer.optimizer_column_registrar';
|
||||
|
||||
public function register(): void {
|
||||
$this->container->singleton( Optimizer_Renderer::class, Optimizer_Renderer::class );
|
||||
|
||||
// Register the renderer.
|
||||
$this->container->when( Optimizer_Renderer::class )
|
||||
->needs( Store::class )
|
||||
->give(
|
||||
function () {
|
||||
$store = $this->container->get( Table_Store::class );
|
||||
|
||||
return new Cached_Store_Decorator( $store );
|
||||
}
|
||||
);
|
||||
|
||||
// Register the column.
|
||||
$this->container->singleton(
|
||||
self::OPTIMIZER_COLUMN,
|
||||
static fn(): Column => new Column(
|
||||
'kadence_optimizer',
|
||||
__( 'Kadence Optimizer', 'kadence-blocks' ),
|
||||
Meta::KEY
|
||||
)
|
||||
);
|
||||
|
||||
// Register the column registrar for this column.
|
||||
$this->container->singleton(
|
||||
self::OPTIMIZER_COLUMN_REGISTRAR,
|
||||
function (): Column_Registrar {
|
||||
$column = $this->container->get( self::OPTIMIZER_COLUMN );
|
||||
$renderer = $this->container->get( Optimizer_Renderer::class );
|
||||
|
||||
return new Column_Registrar( $column, $renderer );
|
||||
}
|
||||
);
|
||||
|
||||
// Register the hook manager for all configured columns.
|
||||
$this->container->singleton( Column_Hook_Manager::class, Column_Hook_Manager::class );
|
||||
|
||||
$this->container
|
||||
->when( Column_Hook_Manager::class )
|
||||
->needs( '$columns' )
|
||||
->give(
|
||||
fn(): array => [
|
||||
$this->container->get( self::OPTIMIZER_COLUMN_REGISTRAR ),
|
||||
]
|
||||
);
|
||||
|
||||
// Add post list table columns.
|
||||
add_action(
|
||||
'admin_init',
|
||||
$this->container->callback( Column_Hook_Manager::class, 'register_hooks' )
|
||||
);
|
||||
|
||||
// Bulk action dropdown for bulk optimization/remove optimization functionality.
|
||||
$this->container->singleton( Bulk_Action_Manager::class, Bulk_Action_Manager::class );
|
||||
|
||||
add_filter(
|
||||
'bulk_actions-edit-post',
|
||||
$this->container->callback( Bulk_Action_Manager::class, 'register_actions' )
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'bulk_actions-edit-page',
|
||||
$this->container->callback( Bulk_Action_Manager::class, 'register_actions' )
|
||||
);
|
||||
|
||||
$this->container->singleton( Filter::class, Filter::class );
|
||||
|
||||
add_action(
|
||||
'restrict_manage_posts',
|
||||
$this->container->callback( Filter::class, 'render_filter' )
|
||||
);
|
||||
|
||||
add_action(
|
||||
'pre_get_posts',
|
||||
$this->container->callback( Filter::class, 'filter_posts' )
|
||||
);
|
||||
|
||||
$this->register_row_actions();
|
||||
}
|
||||
|
||||
private function register_row_actions(): void {
|
||||
$this->container->singleton( Row_Action::class, Row_Action::class );
|
||||
|
||||
add_filter(
|
||||
'post_row_actions',
|
||||
$this->container->callback( Row_Action::class, 'add_view_optimized_link' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'page_row_actions',
|
||||
$this->container->callback( Row_Action::class, 'add_view_optimized_link' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Renderers;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Nonce\Nonce;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts\Renderable;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\WebsiteAnalysis;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Status;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Translation\Text_Repository;
|
||||
use KadenceWP\KadenceBlocks\Traits\Permalink_Trait;
|
||||
|
||||
final class Optimizer_Renderer implements Renderable {
|
||||
|
||||
use Permalink_Trait;
|
||||
|
||||
private const ACTION_CLASS = 'action_class';
|
||||
private const ACTION_TEXT = 'action_text';
|
||||
private const STATUS_TEXT = 'status_text';
|
||||
|
||||
private Store $store;
|
||||
private Text_Repository $text_repository;
|
||||
private Nonce $nonce;
|
||||
private Status $status;
|
||||
|
||||
public function __construct(
|
||||
Store $store,
|
||||
Text_Repository $text_repository,
|
||||
Nonce $nonce,
|
||||
Status $status
|
||||
) {
|
||||
$this->store = $store;
|
||||
$this->text_repository = $text_repository;
|
||||
$this->nonce = $nonce;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a link to run/remove optimization data if the current user has the correct
|
||||
* permissions.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render( int $post_id ): void {
|
||||
if ( $this->status->is_excluded( $post_id ) ) {
|
||||
echo esc_html( $this->text_repository->get( Text_Repository::EXCLUDED ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->is_post_optimizable( $post_id ) ) {
|
||||
echo esc_html( $this->text_repository->get( Text_Repository::NOT_OPTIMIZABLE ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$post_path = $this->get_post_path( $post_id );
|
||||
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
$analysis = $this->store->get( $path );
|
||||
$render_data = $this->get_render_data( $analysis );
|
||||
|
||||
// User can't perform an action, just show them the status.
|
||||
if ( ! $this->user_can_manage_optimization( $post_id, $analysis ) ) {
|
||||
echo esc_html( $render_data[ self::STATUS_TEXT ] );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render_action_link( $post_id, $render_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the post can be optimized.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_post_optimizable( int $post_id ): bool {
|
||||
$post_url = get_permalink( $post_id );
|
||||
$status = get_post_status( $post_id );
|
||||
|
||||
return $post_url && ! is_wp_error( $post_url ) && $status === 'publish';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the render data for the optimization action.
|
||||
*
|
||||
* @param WebsiteAnalysis|null $analysis The optimization analysis data.
|
||||
*
|
||||
* @return array{action_class: string, action_text: string, status_text: string}
|
||||
*/
|
||||
private function get_render_data( ?WebsiteAnalysis $analysis ): array {
|
||||
if ( ! $analysis ) {
|
||||
return [
|
||||
self::ACTION_CLASS => 'kb-optimize-post',
|
||||
self::ACTION_TEXT => $this->text_repository->get( Text_Repository::RUN_OPTIMIZER ),
|
||||
self::STATUS_TEXT => $this->text_repository->get( Text_Repository::NOT_OPTIMIZED ),
|
||||
];
|
||||
}
|
||||
|
||||
if ( $analysis->isStale ) {
|
||||
return [
|
||||
self::ACTION_CLASS => 'kb-optimize-post',
|
||||
self::ACTION_TEXT => $this->text_repository->get( Text_Repository::OPTIMIZATION_OUTDATED_RUN ),
|
||||
self::STATUS_TEXT => $this->text_repository->get( Text_Repository::OPTIMIZATION_OUTDATED ),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
self::ACTION_CLASS => 'kb-remove-post-optimization',
|
||||
self::ACTION_TEXT => $this->text_repository->get( Text_Repository::REMOVE_OPTIMIZATION ),
|
||||
self::STATUS_TEXT => $this->text_repository->get( Text_Repository::OPTIMIZED ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can manage optimization for this post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param WebsiteAnalysis|null $analysis The optimization analysis data.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function user_can_manage_optimization( int $post_id, ?WebsiteAnalysis $analysis ): bool {
|
||||
return $analysis
|
||||
? current_user_can( 'delete_post', $post_id )
|
||||
: current_user_can( 'edit_post', $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the action link for optimization management.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param array $render_data The render data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_action_link( int $post_id, array $render_data ): void {
|
||||
$post_url = get_permalink( $post_id );
|
||||
$post_path = $this->get_post_path( $post_id );
|
||||
|
||||
printf(
|
||||
'<a href="#" class="%s" data-post-id="%d" data-post-url="%s" data-post-path="%s" data-nonce="%s">%s</a>',
|
||||
esc_attr( $render_data[ self::ACTION_CLASS ] ),
|
||||
esc_attr( $post_id ),
|
||||
esc_attr( esc_url( $post_url ) ),
|
||||
esc_attr( $post_path ),
|
||||
esc_attr( $this->nonce->create() ),
|
||||
esc_html( $render_data[ self::ACTION_TEXT ] )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Nonce\Nonce;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Request\Request;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Traits\Permalink_Trait;
|
||||
use WP_Post;
|
||||
|
||||
final class Row_Action {
|
||||
|
||||
use Permalink_Trait;
|
||||
|
||||
public const ACTION = 'kb_optimizer_view_optimized';
|
||||
|
||||
private Nonce $nonce;
|
||||
private Store $store;
|
||||
|
||||
public function __construct(
|
||||
Nonce $nonce,
|
||||
Store $store
|
||||
) {
|
||||
$this->nonce = $nonce;
|
||||
$this->store = $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "View Optimized" link to the post/page row actions.
|
||||
*
|
||||
* @filter post_row_actions
|
||||
* @filter page_row_actions
|
||||
*
|
||||
* @param string[] $actions An array of row actions links.
|
||||
* @param WP_Post $post The post object.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function add_view_optimized_link( array $actions, WP_Post $post ): array {
|
||||
$permalink = get_permalink( $post->ID );
|
||||
|
||||
if ( ! $permalink ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$post_path = $this->get_post_path( $post->ID );
|
||||
|
||||
try {
|
||||
$path = new Path( $post_path, $post->ID );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$analysis = $this->store->get( $path );
|
||||
|
||||
if ( ! $analysis || $analysis->isStale ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$url = add_query_arg(
|
||||
[
|
||||
Request::QUERY_TOKEN => $this->nonce->create(),
|
||||
Request::QUERY_OPTIMIZER_PREVIEW => 1,
|
||||
],
|
||||
$permalink
|
||||
);
|
||||
|
||||
$actions[ self::ACTION ] = sprintf(
|
||||
'<a href="%s" target="_blank">%s</a>',
|
||||
esc_url( $url ),
|
||||
esc_html__( 'View Optimized', 'kadence-blocks' )
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Sorters;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Column;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts\Sort_Strategy;
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* Sort by a meta key, including if the key does not or does exist.
|
||||
*/
|
||||
final class Meta_Sort_Exists implements Sort_Strategy {
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $secondary_order_fields;
|
||||
|
||||
/**
|
||||
* @param string[] $secondary_order_fields The field names to order by after the meta value.
|
||||
*/
|
||||
public function __construct(
|
||||
array $secondary_order_fields = [
|
||||
'title',
|
||||
]
|
||||
) {
|
||||
$this->secondary_order_fields = $secondary_order_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a column by whether a meta key exists or not.
|
||||
*
|
||||
* @filter pre_get_posts
|
||||
*
|
||||
* @param WP_Query $query
|
||||
* @param Column $column
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sort( WP_Query $query, Column $column ): void {
|
||||
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $query->get( 'orderby' ) !== $column->slug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we don't filter out all posts that don't have the value.
|
||||
$query->set(
|
||||
'meta_query',
|
||||
[
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => $column->meta_key,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => $column->meta_key,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Order by meta_value and tack on any secondary fields.
|
||||
$query->set(
|
||||
'orderby',
|
||||
trim(
|
||||
sprintf(
|
||||
'meta_value %s',
|
||||
implode( ' ', $this->secondary_order_fields )
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Sorters;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Column;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Post_List_Table\Contracts\Sort_Strategy;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Status;
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* Custom column sorting strategy for the Kadence Optimizer.
|
||||
*/
|
||||
final class Optimizer_Sorter implements Sort_Strategy {
|
||||
|
||||
/**
|
||||
* Custom column sorter for the Kadence Optimizer.
|
||||
*
|
||||
* Uses integer status values that sort naturally:
|
||||
* - ASC: Excluded (-1) > Not Optimized (0) > Optimized (1) > Stale (2)
|
||||
* - DESC: Stale (2) > Optimized (1) > Not Optimized (0) > Excluded (-1)
|
||||
*
|
||||
* @filter pre_get_posts
|
||||
*
|
||||
* @see Status
|
||||
*
|
||||
* @param WP_Query $query
|
||||
* @param Column $column
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sort( WP_Query $query, Column $column ): void {
|
||||
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $query->get( 'orderby' ) !== $column->slug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = strtoupper( (string) $query->get( 'order' ) ?: 'DESC' );
|
||||
|
||||
// Only published posts can be worked with, so hide the rest when sorting.
|
||||
$query->set( 'post_status', 'publish' );
|
||||
|
||||
add_filter(
|
||||
'posts_clauses',
|
||||
static function ( array $clauses ) use ( $order, $column ): array {
|
||||
global $wpdb;
|
||||
|
||||
// Join optimizer status meta - single meta key for all states.
|
||||
$clauses['join'] .= $wpdb->prepare(
|
||||
" LEFT JOIN $wpdb->postmeta AS pm_optimizer_status
|
||||
ON ($wpdb->posts.ID = pm_optimizer_status.post_id AND pm_optimizer_status.meta_key = %s)",
|
||||
$column->meta_key
|
||||
);
|
||||
|
||||
// Sort by status value (defaults to 0 for unoptimized posts).
|
||||
$clauses['orderby'] = sprintf(
|
||||
'CAST(IFNULL(pm_optimizer_status.meta_value, 0) AS SIGNED) %s',
|
||||
$order
|
||||
);
|
||||
|
||||
$clauses['distinct'] = 'DISTINCT';
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
# Kadence Performance Optimizer
|
||||
|
||||
A client-side analysis and server-side optimization system that improves Core Web Vitals (LCP, CLS, INP) by intelligently managing image loading, section rendering, and resource hints.
|
||||
|
||||
## Overview
|
||||
|
||||
The Optimizer works in two phases:
|
||||
|
||||
1. **Analysis Phase** (client-side): When a user triggers optimization in wp-admin, JavaScript analyzes the page using `@stellarwp/perf-analyzer-client` to detect critical images, section heights, and optimal image sizes for both desktop and mobile viewports.
|
||||
|
||||
2. **Application Phase** (server-side): On subsequent requests, the stored analysis data is used to apply optimizations like proper lazy loading, `content-visibility` for below-the-fold sections, and optimal image `sizes` attributes.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Optimizer/
|
||||
├── Database/ # Custom tables (kb_optimizer, kb_optimizer_viewport_hash)
|
||||
├── Store/ # Decorator-based storage layer
|
||||
├── Hash/ # HTML hash comparison for invalidation
|
||||
├── Image/ # Image processing pipeline
|
||||
├── Lazy_Load/ # Element & background lazy loading
|
||||
├── Skip_Rules/ # Conditions to bypass optimization
|
||||
├── Rest/ # REST API endpoints
|
||||
├── Response/ # DTOs (WebsiteAnalysis, DeviceAnalysis, ImageAnalysis, etc.)
|
||||
├── Status/ # Post optimization status tracking
|
||||
├── Post_List_Table/ # Admin column & bulk actions
|
||||
└── assets/js/ # Frontend analyzer scripts
|
||||
```
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Provider Pattern**: Each subdomain has a `Provider.php` that registers dependencies
|
||||
- **Decorator Pattern**: Store layer uses decorators for caching, expiration, exclusion, and status sync
|
||||
- **Strategy Pattern**: Image processors and skip rules are pluggable
|
||||
|
||||
## Enabling the Optimizer
|
||||
|
||||
Users enable the optimizer through the **Kadence Blocks Controls** sidebar in the Gutenberg editor:
|
||||
|
||||
1. Open any post/page in the block editor
|
||||
2. Click the Kadence "K" icon in the top toolbar to open **Kadence Blocks Controls**
|
||||
3. Expand the **Performance Optimizer** panel
|
||||
4. Toggle **"Globally Enable The Performance Optimizer"**
|
||||
|
||||
This saves the `performance_optimizer_enabled` setting to `kadence_blocks_settings` option.
|
||||
|
||||
## Store Decorator Chain
|
||||
|
||||
The `Store` interface is wrapped in decorators that execute top-to-bottom:
|
||||
|
||||
```
|
||||
Expired_Store_Decorator ← Filters stale data (isStale = true)
|
||||
└─ Cached_Store_Decorator ← Request-level memoization
|
||||
└─ Excluded_Store_Decorator ← Blocks excluded posts
|
||||
└─ Status_Sync_Store_Decorator ← Syncs post meta status
|
||||
└─ Table_Store ← Database operations
|
||||
```
|
||||
|
||||
This means:
|
||||
- Expired data is filtered even from cache
|
||||
- Excluded posts never receive optimization data
|
||||
- Post meta status stays in sync with database state
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
Base: `/wp-json/kb-optimizer/v1/optimize`
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/optimize` | Store analysis data for a post |
|
||||
| GET | `/optimize` | Retrieve analysis data |
|
||||
| DELETE | `/optimize` | Remove optimization for a post |
|
||||
| POST | `/optimize/posts-metadata` | Get metadata for bulk optimization |
|
||||
| DELETE | `/optimize/bulk/delete` | Bulk remove optimizations |
|
||||
|
||||
All endpoints require `edit_post` capability for the target post(s).
|
||||
|
||||
## Skip Rules
|
||||
|
||||
Optimization is bypassed when any skip rule returns `true`:
|
||||
|
||||
| Rule | Condition |
|
||||
|------|-----------|
|
||||
| `Optimizer_Request_Rule` | Request has optimizer query params |
|
||||
| `Ignored_Query_Var_Rule` | Has preview or filtered query vars |
|
||||
| `Not_Found_Rule` | 404 response |
|
||||
| `Logged_In_Rule` | User is logged in |
|
||||
| `Post_Excluded_Rule` | Post marked as excluded |
|
||||
|
||||
### Adding Ignored Query Variables
|
||||
|
||||
The `Ignored_Query_Var_Rule` skips optimization when specific query parameters are present. Add custom query vars to ignore:
|
||||
|
||||
```php
|
||||
add_filter( 'kadence_blocks_optimizer_rule_skip_query_vars', static fn( array $vars ): array => [
|
||||
...$vars,
|
||||
'my_custom_preview',
|
||||
]);
|
||||
```
|
||||
|
||||
## Image Processing
|
||||
|
||||
The `Image_Processor` uses output buffering to modify `<img>` tags:
|
||||
|
||||
1. **Lazy_Load_Processor**: Removes `loading="lazy"` from critical (above-fold) images, adds it to below-fold images
|
||||
2. **Sizes_Attribute_Processor**: Sets optimal `sizes` attribute based on actual rendered dimensions
|
||||
|
||||
### Filtering Image Processing
|
||||
|
||||
```php
|
||||
add_filter(
|
||||
'kadence_blocks_optimizer_image_processor',
|
||||
static fn( bool $should, string $src ): bool => $should && ! str_contains( $src, 'logo.png' ),
|
||||
10,
|
||||
5
|
||||
);
|
||||
```
|
||||
|
||||
## Section Lazy Loading
|
||||
|
||||
Below-the-fold Kadence Row and Column blocks receive `content-visibility: auto` with calculated `contain-intrinsic-size`:
|
||||
|
||||
```php
|
||||
// Exclude specific classes from section lazy loading.
|
||||
add_filter( 'kadence_blocks_optimizer_section_lazy_load_excluded_classes', static fn( array $classes ): array => [
|
||||
...$classes,
|
||||
'my-parallax-section',
|
||||
]);
|
||||
```
|
||||
|
||||
**Note**: `kt-jarallax` is excluded by default since parallax sections need immediate rendering.
|
||||
|
||||
## Hash-Based Invalidation
|
||||
|
||||
The system compares HTML hashes on each request to detect content changes:
|
||||
|
||||
1. After optimization, hashes are stored for desktop and mobile viewports
|
||||
2. On subsequent requests, current HTML is hashed and compared
|
||||
3. If hashes differ, analysis is marked `isStale = true`
|
||||
4. Stale data is filtered by `Expired_Store_Decorator`, forcing re-optimization
|
||||
|
||||
### Triggering Hash Storage
|
||||
|
||||
```
|
||||
?kadence_set_optimizer_hash=1 # Desktop hash
|
||||
?kadence_set_optimizer_hash=1&kadence_is_mobile=1 # Mobile hash
|
||||
```
|
||||
|
||||
## Post Status Values
|
||||
|
||||
Status is stored in post meta (`_kb_optimizer_status`):
|
||||
|
||||
| Value | Constant | Description |
|
||||
|-------|----------|-------------|
|
||||
| -1 | `EXCLUDED` | User excluded from optimization |
|
||||
| 0 | `NOT_OPTIMIZED` | No optimization data |
|
||||
| 1 | `OPTIMIZED` | Active optimization |
|
||||
| 2 | `STALE` | Outdated, pending re-optimization |
|
||||
|
||||
## Actions & Filters Reference
|
||||
|
||||
### Actions
|
||||
|
||||
```php
|
||||
// Fired after hash is stored.
|
||||
do_action( 'kadence_blocks_optimizer_set_hash', $hash, $path, $viewport );
|
||||
|
||||
// Fired when optimization data is invalidated.
|
||||
do_action( 'kadence_blocks_optimizer_data_invalidated', $is_stale, $path );
|
||||
|
||||
// Fired after hash check completes.
|
||||
do_action( 'kadence_blocks_hash_check_complete' );
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
```php
|
||||
// Force enable/disable the entire Optimizer feature, overidding the user's setting.
|
||||
// Force enable.
|
||||
add_filter( 'kadence_blocks_optimizer_enabled', '__return_true' );
|
||||
|
||||
// Force disable.
|
||||
add_filter( 'kadence_blocks_optimizer_enabled', '__return_false' );
|
||||
|
||||
// Customize skip query vars/
|
||||
add_filter( 'kadence_blocks_optimizer_rule_skip_query_vars', fn( array $vars ) => $vars );
|
||||
|
||||
// Exclude sections from lazy loading.
|
||||
add_filter( 'kadence_blocks_optimizer_section_lazy_load_excluded_classes', fn( array $classes ) => $classes );
|
||||
|
||||
// Control image processing per-image.
|
||||
add_filter( 'kadence_blocks_optimizer_image_processor', fn( bool $should, $src, $classes, $processor, $path ) => $should, 10, 5 );
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Set `WP_DEBUG` to `true` in `wp-config.php` to enable comprehensive logging via PHP's `error_log()`:
|
||||
|
||||
```php
|
||||
define( 'WP_DEBUG', true );
|
||||
```
|
||||
|
||||
Logs are prefixed with `[Kadence Blocks]:` and written to your PHP error log (location depends on server configuration).
|
||||
|
||||
### Check if a page is optimized
|
||||
|
||||
Look for the `kb-optimized` class on the `<body>` tag.
|
||||
|
||||
### View optimization data
|
||||
|
||||
```php
|
||||
$store = KadenceWP\KadenceBlocks\Container::get(
|
||||
\KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store::class
|
||||
);
|
||||
$path = new \KadenceWP\KadenceBlocks\Optimizer\Path\Path( '/my-page/', $post_id );
|
||||
$analysis = $store->get( $path );
|
||||
```
|
||||
|
||||
### Preview optimized view
|
||||
|
||||
Add `?kadence_optimizer_preview=1` to any URL to see the optimized version regardless of skip rules (logged-in users, etc.).
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Optimization not applying
|
||||
|
||||
1. Check skip rules - logged-in users are skipped by default
|
||||
2. Verify post is published (drafts can't be optimized)
|
||||
3. Check if post is excluded (`Status::EXCLUDED`)
|
||||
4. Look for `isStale = true` in stored analysis
|
||||
|
||||
### Content-visibility causing layout shifts
|
||||
|
||||
Exclude problematic sections via the filter:
|
||||
|
||||
```php
|
||||
add_filter( 'kadence_blocks_optimizer_section_lazy_load_excluded_classes', static fn( array $classes ): array => [
|
||||
...$classes,
|
||||
'my-problematic-section',
|
||||
]);
|
||||
```
|
||||
|
||||
### Images loading incorrectly
|
||||
|
||||
The system uses a queue-based approach for critical images. If the same image appears multiple times, only the first N occurrences (matching the critical images count) are treated as critical.
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Request;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
$this->register_request();
|
||||
$this->register_request_anonymizer();
|
||||
}
|
||||
|
||||
private function register_request(): void {
|
||||
$this->container->singleton( Request::class, Request::class );
|
||||
}
|
||||
|
||||
private function register_request_anonymizer(): void {
|
||||
$this->container->singleton( Request_Anonymizer::class, Request_Anonymizer::class );
|
||||
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
function () {
|
||||
$this->container->get( Request_Anonymizer::class )->force_anonymous_request();
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Request;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
|
||||
final class Request {
|
||||
|
||||
public const QUERY_NOCACHE = 'nocache';
|
||||
public const QUERY_TOKEN = 'perf_token';
|
||||
public const QUERY_OPTIMIZER_PREVIEW = 'kb_optimizer_preview';
|
||||
public const MIN_LENGTH = 5;
|
||||
|
||||
/**
|
||||
* Check if this request is an optimizer request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_optimizer_request(): bool {
|
||||
$no_cache = SG::get_get_var( self::QUERY_NOCACHE );
|
||||
$token = SG::get_get_var( self::QUERY_TOKEN );
|
||||
|
||||
return $this->is_valid_param( $no_cache ) && $this->is_valid_param( $token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a request parameter: must be an alphanumeric string and a minimum length.
|
||||
*
|
||||
* @param mixed $value The query parameter value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_valid_param( $value ): bool {
|
||||
return is_string( $value ) && strlen( $value ) >= self::MIN_LENGTH && ctype_alnum( $value );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Request;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Nonce\Nonce;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
|
||||
/**
|
||||
* Handles the anonymization of authenticated requests for performance optimization.
|
||||
*
|
||||
* This class detects optimizer requests using a nonce-based query parameter and
|
||||
* forces them to run as anonymous (logged-out) requests. This ensures that
|
||||
* performance testing through iframes captures the public-facing version of
|
||||
* pages without any user-specific customizations or cached content.
|
||||
*/
|
||||
final class Request_Anonymizer {
|
||||
|
||||
private Nonce $nonce;
|
||||
|
||||
public function __construct( Nonce $nonce ) {
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if this is an optimizer request, and log the user out for that request so
|
||||
* we're loading the public version of the URL through the iframe.
|
||||
*
|
||||
* @filter plugins_loaded
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function force_anonymous_request(): void {
|
||||
$nonce = SG::get_get_var( Request::QUERY_TOKEN );
|
||||
|
||||
if ( ! $nonce ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If using a browser that supports IFrame credentialless, they will be logged out already.
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->nonce->verify( $nonce ) ) {
|
||||
wp_die( 'Invalid optimizer nonce.', 403 );
|
||||
}
|
||||
|
||||
// Override the current user for the remainder of this request.
|
||||
wp_set_current_user( 0 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Resource_Hints;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Analysis_Registry;
|
||||
|
||||
/**
|
||||
* Adds the following to the frontend head section, to allow faster loading
|
||||
* Google Fonts:
|
||||
*
|
||||
* <link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
* <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
*/
|
||||
final class Google_Font_Preconnector {
|
||||
|
||||
public const PRECONNECT = 'preconnect';
|
||||
|
||||
private Analysis_Registry $registry;
|
||||
|
||||
public function __construct(
|
||||
Analysis_Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add preconnect links for Google Fonts. We actually have no way of knowing that Kadence is using
|
||||
* them as the style enqueuing happens later in the execution.
|
||||
*
|
||||
* @filter wp_resource_hints
|
||||
*
|
||||
* @param array<array|string> $urls Array of resources and their attributes, or URLs to print for resource hints.
|
||||
* @param string $relation_type The relation type the URLs are printed for. One of 'dns-prefetch', 'preconnect', 'prefetch', or 'prerender'.
|
||||
*
|
||||
* @return array<array|string>
|
||||
*/
|
||||
public function preconnect( array $urls, string $relation_type ): array {
|
||||
if ( ! $this->registry->is_optimized() ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
if ( self::PRECONNECT !== $relation_type ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
$urls[] = 'https://fonts.googleapis.com';
|
||||
$urls[] = [
|
||||
'href' => 'https://fonts.gstatic.com',
|
||||
true => 'crossorigin',
|
||||
];
|
||||
|
||||
return $urls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Resource_Hints;
|
||||
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
/**
|
||||
* Allow resource hints to be disabled.
|
||||
*
|
||||
* @param bool $enabled Whether this feature is enabled.
|
||||
*/
|
||||
$enabled = (bool) apply_filters( 'kadence_blocks_enable_resource_hits', true );
|
||||
|
||||
if ( ! $enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->container->singleton( Google_Font_Preconnector::class, Google_Font_Preconnector::class );
|
||||
|
||||
add_filter(
|
||||
'wp_resource_hints',
|
||||
$this->container->callback( Google_Font_Preconnector::class, 'preconnect' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing CSS computed style properties for an element.
|
||||
*
|
||||
* Contains the computed CSS values for width, height, object-fit, and object-position
|
||||
* properties as extracted from the browser's computed styles.
|
||||
*/
|
||||
final class ComputedStyle {
|
||||
|
||||
public string $width;
|
||||
public string $height;
|
||||
public string $objectFit;
|
||||
public string $objectPosition;
|
||||
|
||||
private function __construct(
|
||||
string $width,
|
||||
string $height,
|
||||
string $objectFit,
|
||||
string $objectPosition
|
||||
) {
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->objectFit = $objectFit;
|
||||
$this->objectPosition = $objectPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{width: string, height: string, objectFit: string, objectPosition: string} $attributes
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
return new self(
|
||||
$attributes['width'],
|
||||
$attributes['height'],
|
||||
$attributes['objectFit'],
|
||||
$attributes['objectPosition']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{width: string, height: string, objectFit: string, objectPosition: string}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'objectFit' => $this->objectFit,
|
||||
'objectPosition' => $this->objectPosition,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing analysis data for a specific device viewport.
|
||||
*
|
||||
* Contains device-specific optimization data including critical image counts,
|
||||
* background images, and page sections with their layout information.
|
||||
*/
|
||||
final class DeviceAnalysis {
|
||||
|
||||
/**
|
||||
* A list of image src URLs found above the fold.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public array $criticalImages;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public array $backgroundImages;
|
||||
|
||||
/**
|
||||
* @var Section[]
|
||||
*/
|
||||
public array $sections;
|
||||
|
||||
/**
|
||||
* @param string[] $criticalImages A list of image src URLs found above the fold.
|
||||
* @param string[] $backgroundImages The list of URLs of background images found.
|
||||
* @param Section[] $sections The collection of sections.
|
||||
*/
|
||||
private function __construct(
|
||||
array $criticalImages = [],
|
||||
array $backgroundImages = [],
|
||||
array $sections = []
|
||||
) {
|
||||
$this->criticalImages = $criticalImages;
|
||||
$this->backgroundImages = $backgroundImages;
|
||||
$this->sections = $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{criticalImages: string[], backgroundImages?: string[], sections?: array} $attributes
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
$sections = array_map(
|
||||
static function ( array $result ): Section {
|
||||
return Section::from( $result );
|
||||
},
|
||||
$attributes['sections'] ?? []
|
||||
);
|
||||
|
||||
return new self(
|
||||
$attributes['criticalImages'],
|
||||
$attributes['backgroundImages'] ?? [],
|
||||
$sections
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{criticalImages: string[], backgroundImages?: string[], sections?: array}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'criticalImages' => $this->criticalImages,
|
||||
'backgroundImages' => $this->backgroundImages,
|
||||
'sections' => array_map(
|
||||
static function ( Section $section ): array {
|
||||
return $section->toArray();
|
||||
},
|
||||
$this->sections
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the sections that are below the fold.
|
||||
*
|
||||
* @return Section[]
|
||||
*/
|
||||
public function getBelowTheFoldSections(): array {
|
||||
return array_filter( $this->sections, static fn( Section $section ): bool => ! $section->isAboveFold );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing comprehensive analysis data for an image element.
|
||||
*
|
||||
* Contains detailed information about an image including dimensions, loading attributes,
|
||||
* computed styles, srcset entries, and optimization recommendations such as optimal sizes.
|
||||
*/
|
||||
final class ImageAnalysis {
|
||||
|
||||
public string $path;
|
||||
public string $src;
|
||||
|
||||
/**
|
||||
* @var SrcsetEntry[]
|
||||
*/
|
||||
public array $srcset;
|
||||
public int $width;
|
||||
public int $height;
|
||||
public string $widthAttr;
|
||||
public string $heightAttr;
|
||||
public int $naturalWidth;
|
||||
public int $naturalHeight;
|
||||
public ?float $aspectRatio;
|
||||
public ?string $alt;
|
||||
public string $className;
|
||||
public string $loading;
|
||||
public string $decoding;
|
||||
public ?string $sizes;
|
||||
public ComputedStyle $computedStyle;
|
||||
public string $optimalSizes;
|
||||
|
||||
/**
|
||||
* @param SrcsetEntry[] $srcset The srcset entry.
|
||||
*/
|
||||
private function __construct(
|
||||
string $path,
|
||||
string $src,
|
||||
array $srcset,
|
||||
int $width,
|
||||
int $height,
|
||||
string $widthAttr,
|
||||
string $heightAttr,
|
||||
int $naturalWidth,
|
||||
int $naturalHeight,
|
||||
?float $aspectRatio,
|
||||
?string $alt,
|
||||
string $className,
|
||||
string $loading,
|
||||
string $decoding,
|
||||
?string $sizes,
|
||||
ComputedStyle $computedStyle,
|
||||
string $optimalSizes
|
||||
) {
|
||||
$this->path = $path;
|
||||
$this->src = $src;
|
||||
$this->srcset = $srcset;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->widthAttr = $widthAttr;
|
||||
$this->heightAttr = $heightAttr;
|
||||
$this->naturalWidth = $naturalWidth;
|
||||
$this->naturalHeight = $naturalHeight;
|
||||
$this->aspectRatio = $aspectRatio;
|
||||
$this->alt = $alt;
|
||||
$this->className = $className;
|
||||
$this->loading = $loading;
|
||||
$this->decoding = $decoding;
|
||||
$this->sizes = $sizes;
|
||||
$this->computedStyle = $computedStyle;
|
||||
$this->optimalSizes = $optimalSizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{path: string, src: string, srcset: array, width: int, height: int, widthAttr: string, heightAttr: string, naturalWidth: int, naturalHeight: int, aspectRatio: float, alt: ?string, class: ?string, loading: string, decoding: string, sizes: ?string, computedStyle: array, optimalSizes: string} $attributes
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
$srcset = array_map(
|
||||
static function ( array $entry ): SrcsetEntry {
|
||||
return SrcsetEntry::from( $entry );
|
||||
},
|
||||
$attributes['srcset'] ?? []
|
||||
);
|
||||
|
||||
return new self(
|
||||
$attributes['path'] ?? '',
|
||||
$attributes['src'] ?? '',
|
||||
$srcset,
|
||||
$attributes['width'] ?? 0,
|
||||
$attributes['height'] ?? 0,
|
||||
$attributes['widthAttr'] ?? '',
|
||||
$attributes['heightAttr'] ?? '',
|
||||
$attributes['naturalWidth'] ?? 0,
|
||||
$attributes['naturalHeight'] ?? 0,
|
||||
$attributes['aspectRatio'] ?? null,
|
||||
$attributes['alt'],
|
||||
$attributes['class'] ?? '',
|
||||
$attributes['loading'],
|
||||
$attributes['decoding'],
|
||||
$attributes['sizes'],
|
||||
ComputedStyle::from( $attributes['computedStyle'] ),
|
||||
$attributes['optimalSizes']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{path: string, src: string, srcset: array, width: int, height: int, widthAttr: string, heightAttr: string, naturalWidth: int, naturalHeight: int, aspectRatio: float, alt: ?string, class: string, loading: string, decoding: string, sizes: ?string, computedStyle: array, optimalSizes: string}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'path' => $this->path,
|
||||
'src' => $this->src,
|
||||
'srcset' => array_map(
|
||||
static function ( SrcsetEntry $entry ): array {
|
||||
return $entry->toArray();
|
||||
},
|
||||
$this->srcset
|
||||
),
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'widthAttr' => $this->widthAttr,
|
||||
'heightAttr' => $this->heightAttr,
|
||||
'naturalWidth' => $this->naturalWidth,
|
||||
'naturalHeight' => $this->naturalHeight,
|
||||
'aspectRatio' => $this->aspectRatio,
|
||||
'alt' => $this->alt,
|
||||
'class' => $this->className,
|
||||
'loading' => $this->loading,
|
||||
'decoding' => $this->decoding,
|
||||
'sizes' => $this->sizes,
|
||||
'computedStyle' => $this->computedStyle->toArray(),
|
||||
'optimalSizes' => $this->optimalSizes,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing a page section or element with layout information.
|
||||
*
|
||||
* Contains section metadata including dimensions, DOM path, fold position,
|
||||
* and content flags for images and background elements.
|
||||
*/
|
||||
final class Section {
|
||||
|
||||
public string $id;
|
||||
public float $height;
|
||||
public string $tagName;
|
||||
public string $className;
|
||||
public string $path;
|
||||
public bool $isAboveFold;
|
||||
public bool $hasImages;
|
||||
public bool $hasBackground;
|
||||
|
||||
private function __construct(
|
||||
string $id,
|
||||
float $height,
|
||||
string $tagName,
|
||||
string $className,
|
||||
string $path,
|
||||
bool $isAboveFold,
|
||||
bool $hasImages,
|
||||
bool $hasBackground
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->height = $height;
|
||||
$this->tagName = $tagName;
|
||||
$this->className = $className;
|
||||
$this->path = $path;
|
||||
$this->isAboveFold = $isAboveFold;
|
||||
$this->hasImages = $hasImages;
|
||||
$this->hasBackground = $hasBackground;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id: string, height: float, tagName: string, className: string, path: string, isAboveFold: bool, hasImages: bool, hasBackground: bool} $attributes
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
return new self(
|
||||
$attributes['id'],
|
||||
$attributes['height'],
|
||||
$attributes['tagName'],
|
||||
$attributes['className'],
|
||||
$attributes['path'],
|
||||
$attributes['isAboveFold'],
|
||||
$attributes['hasImages'],
|
||||
$attributes['hasBackground']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, height: float, tagName: string, className: string, path: string, isAboveFold: bool, hasImages: bool, hasBackground: bool}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'height' => $this->height,
|
||||
'tagName' => $this->tagName,
|
||||
'className' => $this->className,
|
||||
'path' => $this->path,
|
||||
'isAboveFold' => $this->isAboveFold,
|
||||
'hasImages' => $this->hasImages,
|
||||
'hasBackground' => $this->hasBackground,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing a single entry in an image srcset attribute.
|
||||
*
|
||||
* Contains the URL and width descriptor for a responsive image source,
|
||||
* used to build complete srcset attributes for optimized image delivery.
|
||||
*/
|
||||
final class SrcsetEntry {
|
||||
|
||||
public string $url;
|
||||
public int $width;
|
||||
|
||||
private function __construct( string $url, int $width ) {
|
||||
$this->url = $url;
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{url: string, width: int} $attributes
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
return new self(
|
||||
$attributes['url'],
|
||||
$attributes['width']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{url: string, width: int}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'url' => $this->url,
|
||||
'width' => $this->width,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Response;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Enums\Viewport;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing comprehensive website performance analysis.
|
||||
*
|
||||
* Contains the complete optimization analysis including device-specific data
|
||||
* for desktop and mobile viewports, plus detailed analysis for all images found.
|
||||
*/
|
||||
final class WebsiteAnalysis {
|
||||
|
||||
public bool $isStale;
|
||||
public DeviceAnalysis $desktop;
|
||||
public DeviceAnalysis $mobile;
|
||||
|
||||
/**
|
||||
* @var ImageAnalysis[]
|
||||
*/
|
||||
public array $images;
|
||||
|
||||
/**
|
||||
* @param ImageAnalysis[] $images The image analysis collection.
|
||||
*/
|
||||
private function __construct(
|
||||
bool $isStale,
|
||||
DeviceAnalysis $desktop,
|
||||
DeviceAnalysis $mobile,
|
||||
array $images = []
|
||||
) {
|
||||
$this->isStale = $isStale;
|
||||
$this->desktop = $desktop;
|
||||
$this->mobile = $mobile;
|
||||
$this->images = $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{isStale?: bool, desktop: array, mobile: array, images: array} $attributes
|
||||
*
|
||||
* @throws \Exception If we fail to create a DateTimeImmutable object.
|
||||
*/
|
||||
public static function from( array $attributes ): self {
|
||||
$images = array_map(
|
||||
static function ( array $result ): ImageAnalysis {
|
||||
return ImageAnalysis::from( $result );
|
||||
},
|
||||
$attributes['images'] ?? []
|
||||
);
|
||||
|
||||
$isStale = (bool) ( $attributes['isStale'] ?? false );
|
||||
|
||||
return new self(
|
||||
$isStale,
|
||||
DeviceAnalysis::from( $attributes['desktop'] ),
|
||||
DeviceAnalysis::from( $attributes['mobile'] ),
|
||||
$images
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{isStale: bool, desktop: array, mobile: array, images: array}
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'isStale' => $this->isStale,
|
||||
'desktop' => $this->desktop->toArray(),
|
||||
'mobile' => $this->mobile->toArray(),
|
||||
'images' => array_map(
|
||||
static function ( ImageAnalysis $image ): array {
|
||||
return $image->toArray();
|
||||
},
|
||||
$this->images
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device analysis for a specific viewport.
|
||||
*
|
||||
* @param Viewport $vp The viewport to fetch data for.
|
||||
*
|
||||
* @return DeviceAnalysis|null
|
||||
*/
|
||||
public function getDevice( Viewport $vp ): ?DeviceAnalysis {
|
||||
return $this->{$vp} ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Rest;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Response\WebsiteAnalysis;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Status;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Traits\Permalink_Trait;
|
||||
use WP_Error;
|
||||
use WP_Http;
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* REST API controller for managing Kadence Blocks optimizer data.
|
||||
*
|
||||
* This controller provides endpoints for storing, retrieving, and deleting
|
||||
* performance optimization analysis data for posts. It handles website analysis
|
||||
* results that can be used to optimize page performance.
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /kb-optimizer/v1/optimize - Store optimizer data for a post
|
||||
* - GET /kb-optimizer/v1/optimize - Retrieve optimizer data for a post
|
||||
* - DELETE /kb-optimizer/v1/optimize - Delete optimizer data for a post
|
||||
* - POST /kb-optimizer/v1/optimize/posts-metadata - Get metadata for multiple posts
|
||||
* - DELETE /kb-optimizer/v1/optimize/bulk/delete - Delete optimizer data for multiple posts
|
||||
*
|
||||
* All endpoints require the user to have edit permissions for the specified post.
|
||||
*/
|
||||
final class Optimize_Rest_Controller extends WP_REST_Controller {
|
||||
|
||||
use Permalink_Trait;
|
||||
|
||||
public const ROUTE = '/kb-optimizer/v1/optimize';
|
||||
public const POST_PATH = 'post_path';
|
||||
public const POST_ID = 'post_id';
|
||||
public const POST_IDS = 'post_ids';
|
||||
public const POST_PATHS = 'post_paths';
|
||||
public const RESULTS = 'results';
|
||||
|
||||
private Store $store;
|
||||
private Schema $schema_loader;
|
||||
private Status $status;
|
||||
|
||||
public function __construct(
|
||||
Store $store,
|
||||
Schema $schema_loader,
|
||||
Status $status
|
||||
) {
|
||||
$this->namespace = 'kb-optimizer/v1';
|
||||
$this->rest_base = 'optimize';
|
||||
$this->store = $store;
|
||||
$this->schema_loader = $schema_loader;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
"/$this->rest_base",
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'create_item' ],
|
||||
'permission_callback' => [ $this, 'create_item_permissions_check' ],
|
||||
'args' => $this->get_create_params(),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_item' ],
|
||||
'permission_callback' => [ $this, 'get_item_permissions_check' ],
|
||||
'args' => $this->get_default_params(),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => [ $this, 'delete_item' ],
|
||||
'permission_callback' => [ $this, 'delete_item_permissions_check' ],
|
||||
'args' => $this->get_default_params(),
|
||||
],
|
||||
'schema' => [ $this->schema_loader, 'get_item_schema' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
"/$this->rest_base/posts-metadata",
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_posts_metadata' ],
|
||||
'permission_callback' => [ $this, 'get_posts_metadata_permissions_check' ],
|
||||
'args' => $this->get_posts_metadata_params(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
"/$this->rest_base/bulk/delete",
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => [ $this, 'bulk_delete_items' ],
|
||||
'permission_callback' => [ $this, 'bulk_delete_items_permissions_check' ],
|
||||
'args' => $this->get_bulk_delete_params(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save optimizer data for a post.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function create_item( $request ) {
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
$post_path = $request->get_param( self::POST_PATH );
|
||||
$results = $request->get_param( self::RESULTS );
|
||||
$analysis = WebsiteAnalysis::from( $results );
|
||||
|
||||
if ( $this->status->is_excluded( $post_id ) ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_create_failed_excluded',
|
||||
__( 'This post is excluded from optimization.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::INTERNAL_SERVER_ERROR,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ( get_post_status( $post_id ) !== 'publish' ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_create_failed_not_published',
|
||||
__( 'The post must be published to be optimized.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::INTERNAL_SERVER_ERROR,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
if ( $this->store->set( $path, $analysis ) ) {
|
||||
$ref = wp_get_referer();
|
||||
|
||||
// If we're coming from the edit post page, don't double update the post.
|
||||
if ( $ref && ! str_contains( $ref, 'post.php' ) ) {
|
||||
$this->update_post( $post_id );
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'data' => [
|
||||
'success' => true,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
],
|
||||
],
|
||||
WP_Http::CREATED
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_create_failed',
|
||||
__( 'Failed to store optimizer data.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::INTERNAL_SERVER_ERROR,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimizer data for a post.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) {
|
||||
$post_path = $request->get_param( self::POST_PATH );
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
$analysis = $this->store->get( $path );
|
||||
|
||||
if ( ! $analysis ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_read_not_found',
|
||||
__( 'Sorry, we couldn\'t find optimizer data.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::NOT_FOUND,
|
||||
'post_path' => $post_path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'data' => $analysis->toArray(),
|
||||
],
|
||||
WP_Http::OK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete optimizer data for a post.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function delete_item( $request ) {
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
$post_path = $request->get_param( self::POST_PATH );
|
||||
|
||||
// Include post_id in Path for reliable status synchronization.
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
if ( $this->store->delete( $path ) ) {
|
||||
// Run wp_update post to signal to caching plugins to update their cache.
|
||||
$this->update_post( $post_id );
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'data' => [
|
||||
'success' => true,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
],
|
||||
],
|
||||
WP_Http::OK
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_delete_failed',
|
||||
__( 'Failed to delete optimizer data.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::INTERNAL_SERVER_ERROR,
|
||||
'post_id' => $post_id,
|
||||
'post_path' => $post_path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for multiple posts to prepare for bulk optimization.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_posts_metadata( WP_REST_Request $request ): WP_REST_Response {
|
||||
$post_ids = $request->get_param( self::POST_IDS );
|
||||
|
||||
$posts_data = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
$errors[] = [
|
||||
'post_id' => $post_id,
|
||||
'message' => __( 'Post not found.', 'kadence-blocks' ),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$post_url = get_permalink( $post_id );
|
||||
$post_path = $this->get_post_path( $post_id );
|
||||
|
||||
// Skip posts without pretty permalinks.
|
||||
if ( ! $post_url || empty( $post_path ) || str_contains( $post_path, '?' ) ) {
|
||||
$errors[] = [
|
||||
'post_id' => $post_id,
|
||||
'message' => __( 'Post has no rewrite rules or is not public.', 'kadence-blocks' ),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$posts_data[] = [
|
||||
'post_id' => $post_id,
|
||||
'url' => $post_url,
|
||||
'post_path' => $post_path,
|
||||
];
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'data' => $posts_data,
|
||||
'errors' => $errors,
|
||||
],
|
||||
WP_Http::OK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete optimizer data for multiple posts at once.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function bulk_delete_items( WP_REST_Request $request ): WP_REST_Response {
|
||||
$post_ids = $request->get_param( self::POST_IDS );
|
||||
$post_paths = $request->get_param( self::POST_PATHS );
|
||||
|
||||
$results = [
|
||||
'successful' => [],
|
||||
'failed' => [],
|
||||
];
|
||||
|
||||
foreach ( $post_ids as $index => $post_id ) {
|
||||
$post_path = $post_paths[ $index ] ?? null;
|
||||
|
||||
if ( ! $post_path ) {
|
||||
$results['failed'][] = [
|
||||
'post_id' => $post_id,
|
||||
'message' => __( 'Missing post path.', 'kadence-blocks' ),
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
// Check if optimization data exists.
|
||||
if ( ! $this->store->has( $path ) ) {
|
||||
// No data to delete - consider this a success.
|
||||
$this->store->delete( $path );
|
||||
|
||||
$results['successful'][] = $post_id;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->store->delete( $path ) ) {
|
||||
// Run wp_update_post to signal to caching plugins to update their cache.
|
||||
$this->update_post( $post_id );
|
||||
|
||||
$results['successful'][] = $post_id;
|
||||
} else {
|
||||
$results['failed'][] = [
|
||||
'post_id' => $post_id,
|
||||
'message' => __( 'Failed to delete optimizer data.', 'kadence-blocks' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'data' => $results,
|
||||
],
|
||||
WP_Http::OK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to create items.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has access to create items, WP_Error object
|
||||
* otherwise.
|
||||
*/
|
||||
public function create_item_permissions_check( $request ) {
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
|
||||
if ( $post_id && current_user_can( 'edit_post', $post_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_post_does_not_exist',
|
||||
__( 'Sorry, we could not find that post_id.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::NOT_FOUND,
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_create_forbidden',
|
||||
__( 'Sorry, you are not allowed to store optimizer data.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => rest_authorization_required_code(),
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to get a specific item.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_item_permissions_check( $request ) {
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
|
||||
if ( $post_id && current_user_can( 'edit_post', $post_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_post_does_not_exist',
|
||||
__( 'Sorry, we could not find that post_id.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::NOT_FOUND,
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_read_forbidden',
|
||||
__( 'Sorry, you are not allowed to access optimizer data.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => rest_authorization_required_code(),
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to delete optimization data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has access to delete the item, WP_Error object
|
||||
* otherwise.
|
||||
*/
|
||||
public function delete_item_permissions_check( $request ) {
|
||||
$post_id = $request->get_param( self::POST_ID );
|
||||
|
||||
if ( $post_id && current_user_can( 'delete_post', $post_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_post_does_not_exist',
|
||||
__( 'Sorry, we could not find that post_id.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::NOT_FOUND,
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_delete_forbidden',
|
||||
__( 'Sorry, you do not have permission to delete data for this post.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => rest_authorization_required_code(),
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to get posts metadata.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has access, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_posts_metadata_permissions_check( $request ) {
|
||||
$post_ids = $request->get_param( self::POST_IDS );
|
||||
|
||||
// Admins and editors can edit all posts - skip individual checks.
|
||||
if ( current_user_can( 'edit_others_posts' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For contributors/authors, verify they can edit each post.
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_bulk_forbidden',
|
||||
__( 'Sorry, you are not allowed to access optimizer data for all selected posts.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => rest_authorization_required_code(),
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to bulk delete optimization data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has access, WP_Error object otherwise.
|
||||
*/
|
||||
public function bulk_delete_items_permissions_check( WP_REST_Request $request ) {
|
||||
$post_ids = $request->get_param( self::POST_IDS );
|
||||
|
||||
// Admins and editors can delete all posts - skip individual checks.
|
||||
if ( current_user_can( 'edit_others_posts' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For contributors/authors, verify they can delete each post.
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
if ( ! current_user_can( 'delete_post', $post_id ) ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_bulk_delete_forbidden',
|
||||
__( 'Sorry, you are not allowed to delete optimizer data for all selected posts.', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => rest_authorization_required_code(),
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default arguments for write methods (POST, DELETE).
|
||||
*
|
||||
* Requires both post_path and post_id for reliable status synchronization.
|
||||
*
|
||||
* @return array<string, array{
|
||||
* type: string,
|
||||
* description: string,
|
||||
* minimum: positive-int,
|
||||
* required: true,
|
||||
* sanitize_callback: callable,
|
||||
* }>
|
||||
*/
|
||||
private function get_default_params(): array {
|
||||
return [
|
||||
self::POST_PATH => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'The relative post path', 'kadence-blocks' ),
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
self::POST_ID => [
|
||||
'type' => 'integer',
|
||||
'description' => __( 'The post ID associated with the optimization data.', 'kadence-blocks' ),
|
||||
'minimum' => 1,
|
||||
'required' => true,
|
||||
'sanitize_callback' => static fn( $value ) => (int) $value,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arguments for the create method.
|
||||
*
|
||||
* @return array<string, array{
|
||||
* type: string,
|
||||
* description: string,
|
||||
* minimum: positive-int,
|
||||
* required: true,
|
||||
* sanitize_callback: callable,
|
||||
* properties: array<string, mixed>
|
||||
* }>
|
||||
*/
|
||||
private function get_create_params(): array {
|
||||
$params = $this->get_default_params();
|
||||
|
||||
$params[ self::RESULTS ] = [
|
||||
'type' => 'object',
|
||||
'properties' => rest_get_endpoint_args_for_schema( $this->schema_loader->get_item_schema() ),
|
||||
];
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arguments for the posts metadata endpoint.
|
||||
*
|
||||
* @return array<string, array{
|
||||
* type: string,
|
||||
* description: string,
|
||||
* required: true,
|
||||
* items: array<string, mixed>,
|
||||
* sanitize_callback: callable
|
||||
* }>
|
||||
*/
|
||||
private function get_posts_metadata_params(): array {
|
||||
return [
|
||||
self::POST_IDS => [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Array of post IDs to get metadata for.', 'kadence-blocks' ),
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
],
|
||||
'sanitize_callback' => static fn( $value ) => array_map( 'intval', $value ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arguments for the bulk delete endpoint.
|
||||
*
|
||||
* @return array<string, array{
|
||||
* type: string,
|
||||
* description: string,
|
||||
* required: true,
|
||||
* items: array<string, mixed>,
|
||||
* sanitize_callback: callable
|
||||
* }>
|
||||
*/
|
||||
private function get_bulk_delete_params(): array {
|
||||
return [
|
||||
self::POST_IDS => [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Array of post IDs to delete optimization data for.', 'kadence-blocks' ),
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
],
|
||||
'sanitize_callback' => static fn( $value ) => array_map( 'intval', $value ),
|
||||
],
|
||||
self::POST_PATHS => [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Array of post paths corresponding to the post IDs.', 'kadence-blocks' ),
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'sanitize_callback' => static fn( $value ) => array_map( 'sanitize_text_field', $value ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a post to hopefully force caching plugins to refresh their cache.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
private function update_post( int $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error(
|
||||
'rest_kb_optimizer_update_post_failed',
|
||||
__( 'Unable to find post to update', 'kadence-blocks' ),
|
||||
[
|
||||
'status' => WP_Http::INTERNAL_SERVER_ERROR,
|
||||
'post_id' => $post_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return wp_update_post( $post );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Rest;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Rest\Optimize_Rest_Controller;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
add_action(
|
||||
'rest_api_init',
|
||||
function (): void {
|
||||
$this->container->get( Optimize_Rest_Controller::class )->register_routes();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Rest;
|
||||
|
||||
/**
|
||||
* Schema definition class for the Performance Optimizer REST API endpoints.
|
||||
*/
|
||||
final class Schema {
|
||||
|
||||
/**
|
||||
* Get the parameter schema definitions for the REST API response.
|
||||
*
|
||||
* Defines the structure and validation rules for desktop/mobile viewport data,
|
||||
* including sections, images, and performance metrics used by the optimizer.
|
||||
*
|
||||
* @return array Schema parameter definitions with type validation and descriptions.
|
||||
*/
|
||||
public function get_params(): array {
|
||||
// Definition for the 'section' object, reused for desktop and mobile.
|
||||
$section_properties = [
|
||||
'id' => [
|
||||
'description' => __( 'The ID attribute of the section element.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'height' => [
|
||||
'description' => __( 'The height of the section in pixels.', 'kadence-blocks' ),
|
||||
'type' => 'number',
|
||||
],
|
||||
'tagName' => [
|
||||
'description' => __( 'The HTML tag name of the section element.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'className' => [
|
||||
'description' => __( 'The CSS classes assigned to this section.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'path' => [
|
||||
'description' => __( 'The CSS selector path to the element.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'isAboveFold' => [
|
||||
'description' => __( 'Whether the section is located above the fold.', 'kadence-blocks' ),
|
||||
'type' => 'boolean',
|
||||
],
|
||||
'hasImages' => [
|
||||
'description' => __( 'Whether the section contains any image elements.', 'kadence-blocks' ),
|
||||
'type' => 'boolean',
|
||||
],
|
||||
'hasBackground' => [
|
||||
'description' => __( 'Whether the section has a background image.', 'kadence-blocks' ),
|
||||
'type' => 'boolean',
|
||||
],
|
||||
];
|
||||
|
||||
// Definition for the 'image' object.
|
||||
$image_properties = [
|
||||
'path' => [
|
||||
'description' => __( 'The CSS selector path to the image element.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
'src' => [
|
||||
'description' => __( 'The source URL of the image.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'required' => true,
|
||||
],
|
||||
'srcset' => [
|
||||
'description' => __( 'An array of sources for different resolutions.', 'kadence-blocks' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'url' => [
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'required' => true,
|
||||
],
|
||||
'width' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'width' => [
|
||||
'description' => __( 'The rendered width of the image in pixels.', 'kadence-blocks' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
],
|
||||
'height' => [
|
||||
'description' => __( 'The rendered height of the image in pixels.', 'kadence-blocks' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
],
|
||||
'widthAttr' => [
|
||||
'description' => __( 'The img element width attribute value.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
],
|
||||
'heightAttr' => [
|
||||
'description' => __( 'The img element height attribute value.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
],
|
||||
'naturalWidth' => [
|
||||
'description' => __( 'The intrinsic width of the image file in pixels.', 'kadence-blocks' ),
|
||||
'type' => 'integer',
|
||||
],
|
||||
'naturalHeight' => [
|
||||
'description' => __( 'The intrinsic height of the image file in pixels.', 'kadence-blocks' ),
|
||||
'type' => 'integer',
|
||||
],
|
||||
'aspectRatio' => [
|
||||
'description' => __( 'The aspect ratio of the image.', 'kadence-blocks' ),
|
||||
'type' => 'number',
|
||||
],
|
||||
'alt' => [
|
||||
'description' => __( 'The alt text of the image.', 'kadence-blocks' ),
|
||||
'type' => [ 'string', 'null' ],
|
||||
],
|
||||
'class' => [
|
||||
'description' => __( 'The class attribute of the image element.', 'kadence-blocks' ),
|
||||
'type' => [ 'string', 'null' ],
|
||||
],
|
||||
'loading' => [
|
||||
'description' => __( 'The loading attribute of the image (e.g., \'lazy\', \'eager\', \'auto\').', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'decoding' => [
|
||||
'description' => __( 'The decoding hint for the image (e.g., \'async\', \'sync\', \'auto\').', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'sizes' => [
|
||||
'description' => __( 'The sizes attribute of the image.', 'kadence-blocks' ),
|
||||
'type' => [ 'string', 'null' ],
|
||||
],
|
||||
'computedStyle' => [
|
||||
'description' => __( 'The computed CSS styles of the image element.', 'kadence-blocks' ),
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'width' => [ 'type' => 'string' ],
|
||||
'height' => [ 'type' => 'string' ],
|
||||
'objectFit' => [ 'type' => 'string' ],
|
||||
'objectPosition' => [ 'type' => 'string' ],
|
||||
],
|
||||
],
|
||||
'optimalSizes' => [
|
||||
'description' => __( 'A calculated optimal sizes string.', 'kadence-blocks' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
// Definition for the viewport-specific data (desktop/mobile).
|
||||
$viewport_data_properties = [
|
||||
'criticalImages' => [
|
||||
'description' => __( 'A list of above the fold image URLs.', 'kadence-blocks' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
],
|
||||
'required' => true,
|
||||
],
|
||||
'backgroundImages' => [
|
||||
'description' => __( 'A list of URLs for background images.', 'kadence-blocks' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
],
|
||||
'required' => true,
|
||||
],
|
||||
'sections' => [
|
||||
'description' => __( 'An array of layout sections.', 'kadence-blocks' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => $section_properties,
|
||||
],
|
||||
'required' => true,
|
||||
],
|
||||
];
|
||||
|
||||
// The main schema structure.
|
||||
return [
|
||||
'isStale' => [
|
||||
'description' => __( 'Whether this data is stale or expired.', 'kadence-blocks' ),
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
],
|
||||
'desktop' => [
|
||||
'description' => __( 'Performance and layout data for the desktop viewport.', 'kadence-blocks' ),
|
||||
'type' => 'object',
|
||||
'properties' => $viewport_data_properties,
|
||||
'required' => true,
|
||||
],
|
||||
'mobile' => [
|
||||
'description' => __( 'Performance and layout data for the mobile viewport.', 'kadence-blocks' ),
|
||||
'type' => 'object',
|
||||
'properties' => $viewport_data_properties,
|
||||
'required' => true,
|
||||
],
|
||||
'images' => [
|
||||
'description' => __( 'An array of all image objects found on the page.', 'kadence-blocks' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => $image_properties,
|
||||
],
|
||||
'required' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete JSON Schema item definition.
|
||||
*
|
||||
* Returns a full JSON Schema (draft-07) compliant structure that can be used
|
||||
* for REST API response validation and documentation generation.
|
||||
*
|
||||
* @return array Complete JSON Schema definition with metadata and property specifications.
|
||||
*/
|
||||
public function get_item_schema(): array {
|
||||
return [
|
||||
'$schema' => 'http://json-schema.org/draft-07/schema#',
|
||||
'title' => __( 'Page Analysis Data', 'kadence-blocks' ),
|
||||
'type' => 'object',
|
||||
'properties' => $this->get_params(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules\Ignored_Query_Var_Rule;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules\Logged_In_Rule;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules\Not_Found_Rule;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules\Optimizer_Request_Rule;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules\Post_Excluded_Rule;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\ProphecyMonorepo\Container\Contracts\Provider as Provider_Contract;
|
||||
|
||||
final class Provider extends Provider_Contract {
|
||||
|
||||
public function register(): void {
|
||||
/**
|
||||
* Filter the list of query variables, that if present will bypass
|
||||
* the hash check.
|
||||
*
|
||||
* @param string[] $query_vars The query variable names.
|
||||
*/
|
||||
$query_vars = apply_filters(
|
||||
'kadence_blocks_optimizer_rule_skip_query_vars',
|
||||
[
|
||||
'preview',
|
||||
]
|
||||
);
|
||||
|
||||
$this->container->when( Ignored_Query_Var_Rule::class )
|
||||
->needs( '$query_vars' )
|
||||
->give(
|
||||
static fn(): array => $query_vars
|
||||
);
|
||||
|
||||
$this->container->when( Rule_Collection::class )
|
||||
->needs( '$rules' )
|
||||
->give(
|
||||
fn(): array => [
|
||||
$this->container->get( Optimizer_Request_Rule::class ),
|
||||
$this->container->get( Ignored_Query_Var_Rule::class ),
|
||||
$this->container->get( Not_Found_Rule::class ),
|
||||
$this->container->get( Logged_In_Rule::class ),
|
||||
$this->container->get( Post_Excluded_Rule::class ),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules;
|
||||
|
||||
/**
|
||||
* Holds the collection of rules that determine whether to skip operations.
|
||||
*/
|
||||
final class Rule_Collection {
|
||||
|
||||
/**
|
||||
* @var Skip_Rule[]
|
||||
*/
|
||||
private array $rules;
|
||||
|
||||
/**
|
||||
* @param Skip_Rule[] $rules
|
||||
*/
|
||||
public function __construct( array $rules ) {
|
||||
$this->rules = $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the rules in the collection.
|
||||
*
|
||||
* @return Skip_Rule[]
|
||||
*/
|
||||
public function all(): array {
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Skip_Rule;
|
||||
use KadenceWP\KadenceBlocks\StellarWP\SuperGlobals\SuperGlobals as SG;
|
||||
|
||||
final class Ignored_Query_Var_Rule implements Skip_Rule {
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $query_vars;
|
||||
|
||||
/**
|
||||
* @filter kadence_blocks_optimizer_rule_skip_query_vars
|
||||
*
|
||||
* @param string[] $query_vars The query variable names.
|
||||
*/
|
||||
public function __construct( array $query_vars = [] ) {
|
||||
$this->query_vars = $query_vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function should_skip(): bool {
|
||||
foreach ( $this->query_vars as $query_var ) {
|
||||
if ( SG::get_get_var( $query_var ) !== null ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Skip_Rule;
|
||||
|
||||
final class Logged_In_Rule implements Skip_Rule {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function should_skip(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Skip_Rule;
|
||||
|
||||
final class Not_Found_Rule implements Skip_Rule {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function should_skip(): bool {
|
||||
return is_404();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Request\Request;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Skip_Rule;
|
||||
|
||||
final class Optimizer_Request_Rule implements Skip_Rule {
|
||||
|
||||
private Request $request;
|
||||
|
||||
public function __construct( Request $request ) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function should_skip(): bool {
|
||||
return $this->request->is_optimizer_request();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Rules;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Skip_Rules\Skip_Rule;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Status\Status;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* Skip Optimization if the post was excluded via the
|
||||
* Gutenberg plugins sidebar which stores to post meta.
|
||||
*/
|
||||
final class Post_Excluded_Rule implements Skip_Rule {
|
||||
|
||||
private Status $status;
|
||||
|
||||
public function __construct( Status $status ) {
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function should_skip(): bool {
|
||||
$current_post = get_post();
|
||||
|
||||
if ( ! $current_post instanceof WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->status->is_excluded( $current_post->ID );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Skip_Rules;
|
||||
|
||||
/**
|
||||
* Defines a rule for determining whether to skip a specific operation.
|
||||
*/
|
||||
interface Skip_Rule {
|
||||
|
||||
/**
|
||||
* Whether the result of this rule will skip
|
||||
* the associated operation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_skip(): bool;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer;
|
||||
|
||||
class State {
|
||||
|
||||
public const SETTINGS_KEY = 'performance_optimizer_enabled';
|
||||
|
||||
/**
|
||||
* The default state of the Optimizer if no
|
||||
* settings have been saved.
|
||||
*/
|
||||
public const DEFAULT_STATE = false;
|
||||
|
||||
/**
|
||||
* Whether the Optimizer is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function enabled(): bool {
|
||||
$settings_json = (string) get_option( 'kadence_blocks_settings', '' );
|
||||
|
||||
if ( ! $settings_json ) {
|
||||
return self::DEFAULT_STATE;
|
||||
}
|
||||
|
||||
$settings = json_decode( $settings_json, true );
|
||||
|
||||
if ( ! is_array( $settings ) ) {
|
||||
return self::DEFAULT_STATE;
|
||||
}
|
||||
|
||||
return filter_var(
|
||||
$settings[ self::SETTINGS_KEY ] ?? self::DEFAULT_STATE,
|
||||
FILTER_VALIDATE_BOOL,
|
||||
FILTER_NULL_ON_FAILURE
|
||||
) ?? self::DEFAULT_STATE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php declare( strict_types=1 );
|
||||
|
||||
namespace KadenceWP\KadenceBlocks\Optimizer\Status;
|
||||
|
||||
use KadenceWP\KadenceBlocks\Asset\Asset;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Path\Path;
|
||||
use KadenceWP\KadenceBlocks\Optimizer\Store\Contracts\Store;
|
||||
use KadenceWP\KadenceBlocks\Traits\Permalink_Trait;
|
||||
|
||||
/**
|
||||
* Post Meta to control excluding posts from being optimized set
|
||||
* via a Gutenberg plugin.
|
||||
*
|
||||
* @see kadence-control-plugin.js
|
||||
*/
|
||||
final class Meta {
|
||||
|
||||
use Permalink_Trait;
|
||||
|
||||
public const KEY = '_kb_optimizer_status';
|
||||
|
||||
private Asset $asset;
|
||||
private Status $status;
|
||||
private Store $store;
|
||||
|
||||
public function __construct(
|
||||
Asset $asset,
|
||||
Status $status,
|
||||
Store $store
|
||||
) {
|
||||
$this->asset = $asset;
|
||||
$this->status = $status;
|
||||
$this->store = $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register post meta for the Gutenberg sidebar toggle to exclude a post from optimization.
|
||||
*
|
||||
* @action init
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_meta(): void {
|
||||
register_post_meta(
|
||||
'',
|
||||
self::KEY,
|
||||
[
|
||||
'single' => true,
|
||||
'type' => 'integer',
|
||||
// Excluded is stored a -1.
|
||||
'sanitize_callback' => function ( $value ): int {
|
||||
$value = (int) $value;
|
||||
|
||||
return $this->status->is_valid( $value ) ? $value : 0;
|
||||
},
|
||||
'auth_callback' => static fn(): bool => current_user_can( 'edit_posts' ),
|
||||
'show_in_rest' => [
|
||||
'schema' => [
|
||||
'type' => 'integer',
|
||||
'description' => __( 'Exclude this post from optimization.', 'kadence-blocks' ),
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the optimizer data if this post is excluded.
|
||||
*
|
||||
* @action updated_post_meta
|
||||
*
|
||||
* @param int $post_id The post ID this meta is for.
|
||||
* @param string $meta_key The current meta key.
|
||||
* @param mixed $value The meta value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_clear_optimizer_data( int $post_id, string $meta_key, $value ): void {
|
||||
if ( $meta_key !== self::KEY || $value !== Status::EXCLUDED ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_path = $this->get_post_path( $post_id );
|
||||
|
||||
if ( ! $post_path ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Include post_id for reliable status synchronization.
|
||||
$path = new Path( $post_path, $post_id );
|
||||
|
||||
$this->store->delete( $path );
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user