| 1 |
# Imagify — Engineering Deep Dive |
| 2 |
|
| 3 |
> Version 2.2.8 · PHP 7.3+ · WordPress 5.3+ · PSR-4 · League Container · ActionScheduler |
| 4 |
> |
| 5 |
> Complete process-level reference for plugin engineers. Class hierarchies, call flows, DB schemas, hook signatures, API structures, and concurrency details. |
| 6 |
|
| 7 |
--- |
| 8 |
|
| 9 |
## Table of Contents |
| 10 |
|
| 11 |
1. [](#1-architecture--bootstrappingArchitecture & Bootstrapping](#1-architecture--bootstrapping](#1-architecture--bootstrapping) |
| 12 |
2. [](#2-namespace--psr-4-structureNamespace & PSR-4 Structure](#2-namespace--psr-4-structure](#2-namespace--psr-4-structure) |
| 13 |
3. [](#3-optimization-process--class-hierarchy--call-flowOptimization Process — Class Hierarchy & Call Flow](#3-optimization-process--class-hierarchy--call-flow](#3-optimization-process--class-hierarchy--call-flow) |
| 14 |
4. [](#4-optimizationfile--method-signaturesOptimization\File — Method Signatures](#4-optimizationfile--method-signatures](#4-optimizationfile--method-signatures) |
| 15 |
5. [](#5-api-client--endpoints--requestresponseAPI Client — Endpoints & Request/Response](#5-api-client--endpoints--requestresponse](#5-api-client--endpoints--requestresponse) |
| 16 |
6. [](#6-wordpress-postmeta-keys--data-structuresWordPress Postmeta Keys & Data Structures](#6-wordpress-postmeta-keys--data-structures](#6-wordpress-postmeta-keys--data-structures) |
| 17 |
7. [](#7-settings--option-keys-types--defaultsSettings — Option Keys, Types & Defaults](#7-settings--option-keys-types--defaults](#7-settings--option-keys-types--defaults) |
| 18 |
8. [](#8-database-schemasDatabase Schemas](#8-database-schemas](#8-database-schemas) |
| 19 |
9. [](#9-bulk-optimization--actionscheduler-integrationBulk Optimization — ActionScheduler Integration](#9-bulk-optimization--actionscheduler-integration](#9-bulk-optimization--actionscheduler-integration) |
| 20 |
10. [](#10-concurrency--locking-mechanismsConcurrency & Locking Mechanisms](#10-concurrency--locking-mechanisms](#10-concurrency--locking-mechanisms) |
| 21 |
11. [](#11-picturedisplay--output-buffer-html-rewritePicture\Display — Output Buffer HTML Rewrite](#11-picturedisplay--output-buffer-html-rewrite](#11-picturedisplay--output-buffer-html-rewrite) |
| 22 |
12. [](#12-ajax--admin-post--full-security-tableAJAX & Admin-Post — Full Security Table](#12-ajax--admin-post--full-security-table](#12-ajax--admin-post--full-security-table) |
| 23 |
13. [](#13-wp-cli-commands--full-signaturesWP-CLI Commands — Full Signatures](#13-wp-cli-commands--full-signatures](#13-wp-cli-commands--full-signatures) |
| 24 |
14. [](#14-developer-hooks--exact-signatures--parameter-typesDeveloper Hooks — Exact Signatures & Parameter Types](#14-developer-hooks--exact-signatures--parameter-types](#14-developer-hooks--exact-signatures--parameter-types) |
| 25 |
15. [](#15-scheduled-tasks--cron--actionschedulerScheduled Tasks — Cron & ActionScheduler](#15-scheduled-tasks--cron--actionscheduler](#15-scheduled-tasks--cron--actionscheduler) |
| 26 |
16. [](#16-multisite-handlingMultisite Handling](#16-multisite-handling](#16-multisite-handling) |
| 27 |
17. [](#17-nextgen-gallery-integrationNextGEN Gallery Integration](#17-nextgen-gallery-integration](#17-nextgen-gallery-integration) |
| 28 |
18. [](#18-third-party-integrationsThird-Party Integrations](#18-third-party-integrations](#18-third-party-integrations) |
| 29 |
19. [](#19-quota--account-managementQuota & Account Management](#19-quota--account-management](#19-quota--account-management) |
| 30 |
20. [](#20-roles--capabilitiesRoles & Capabilities](#20-roles--capabilities](#20-roles--capabilities) |
| 31 |
21. [](#21-troubleshooting-tools--internalstatelist--resetTroubleshooting Tools — InternalStateList & Reset](#21-troubleshooting-tools--internalstatelist--reset](#21-troubleshooting-tools--internalstatelist--reset) |
| 32 |
22. [](#22-error-handling-pathsError Handling Paths](#22-error-handling-paths](#22-error-handling-paths) |
| 33 |
23. [](#23-filesystem-operations--pathsFilesystem Operations & Paths](#23-filesystem-operations--paths](#23-filesystem-operations--paths) |
| 34 |
|
| 35 |
--- |
| 36 |
|
| 37 |
## 1. Architecture & Bootstrapping |
| 38 |
|
| 39 |
*Entry point, constants, DI container, service provider chain, and init sequence.* |
| 40 |
|
| 41 |
### Entry Point — `imagify.php` |
| 42 |
|
| 43 |
WordPress loads `imagify.php` during the `plugins_loaded` phase. It defines all constants and registers activation/deactivation hooks before delegating to `inc/main.php`. |
| 44 |
|
| 45 |
```php |
| 46 |
// imagify.php — constants defined at plugin load time |
| 47 |
define( 'IMAGIFY_VERSION', '2.2.8' ); |
| 48 |
define( 'IMAGIFY_SLUG', 'imagify' ); |
| 49 |
define( 'IMAGIFY_FILE', __FILE__ ); |
| 50 |
define( 'IMAGIFY_PATH', realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' ); |
| 51 |
define( 'IMAGIFY_URL', plugin_dir_url( IMAGIFY_FILE ) ); |
| 52 |
define( 'IMAGIFY_ASSETS_IMG_URL', IMAGIFY_URL . 'assets/images/' ); |
| 53 |
define( 'IMAGIFY_MAX_BYTES', 5242880 ); // 5 MB hard limit per image |
| 54 |
define( 'IMAGIFY_INT_MAX', PHP_INT_MAX - 30 ); |
| 55 |
define( 'IMAGIFY_SITE_DOMAIN', 'https://imagify.io' ); |
| 56 |
define( 'IMAGIFY_APP_DOMAIN', 'https://app.imagify.io' ); |
| 57 |
define( 'IMAGIFY_APP_API_URL', IMAGIFY_APP_DOMAIN . '/api/' ); |
| 58 |
``` |
| 59 |
|
| 60 |
### Bootstrap Sequence |
| 61 |
|
| 62 |
`plugins_loaded` → `imagify_init()` → `vendor/autoload.php` → `new Plugin(Container, args)` → `Plugin::init($providers)` → `do_action('imagify_loaded')` |
| 63 |
|
| 64 |
`imagify_init()` lives in `inc/main.php`. It skips execution if `DOING_AUTOSAVE` is defined. The `Plugin` class (`classes/Plugin.php`) receives a **League\Container** instance and the plugin path, then orchestrates the full init sequence: |
| 65 |
|
| 66 |
```php |
| 67 |
// classes/Plugin.php — init sequence (abridged) |
| 68 |
public function init( array $providers ): void { |
| 69 |
// 1. Register shared services |
| 70 |
$this->container->addShared( 'event_manager', fn() => new EventManager() ); |
| 71 |
$this->container->addShared( 'filesystem', fn() => new Imagify_Filesystem() ); |
| 72 |
|
| 73 |
// 2. Include procedural files (functions/, common/, 3rd-party/) |
| 74 |
$this->include_files(); |
| 75 |
|
| 76 |
// 3. Init legacy singletons |
| 77 |
Imagify_Auto_Optimization::get_instance()->init(); |
| 78 |
Imagify_Options::get_instance()->init(); |
| 79 |
Imagify_Data::get_instance()->init(); |
| 80 |
Imagify_Folders_DB::get_instance()->init(); |
| 81 |
Imagify_Files_DB::get_instance()->init(); |
| 82 |
Imagify_Cron_Library_Size::get_instance()->init(); |
| 83 |
Imagify_Cron_Rating::get_instance()->init(); |
| 84 |
Imagify_Cron_Sync_Files::get_instance()->init(); |
| 85 |
Imagify\Auth\Basic::get_instance()->init(); |
| 86 |
Imagify\Job\MediaOptimization::get_instance()->init(); |
| 87 |
Bulk::get_instance()->init(); |
| 88 |
|
| 89 |
// 4. Admin-only classes |
| 90 |
if ( is_admin() ) { ... } |
| 91 |
|
| 92 |
// 5. Register PSR-4 service providers + subscribers |
| 93 |
foreach ( $providers as $service_provider ) { |
| 94 |
$this->container->addServiceProvider( new $service_provider() ); |
| 95 |
$this->load_subscribers( $provider_instance ); |
| 96 |
} |
| 97 |
|
| 98 |
do_action( 'imagify_loaded', $this ); |
| 99 |
} |
| 100 |
``` |
| 101 |
|
| 102 |
### Activation / Deactivation Hooks |
| 103 |
|
| 104 |
| Hook | Handler | What it does | |
| 105 |
|------|---------|-------------| |
| 106 |
| `register_activation_hook` | `imagify_set_activation()` | Sets transient `imagify_activation` with current user ID (TTL 30s). On network: `set_site_transient`. | |
| 107 |
| `register_deactivation_hook` | `imagify_deactivation()` | Deletes `imagify_check_api_version` and `imagify_check_licence_1` site transients; fires `imagify_deactivation` action. | |
| 108 |
| `init` (Plugin) | `Plugin::maybe_activate()` | Reads activation transient; fires `imagify_activation` action with user ID, then deletes transient. | |
| 109 |
|
| 110 |
### Service Providers (`config/providers.php`) |
| 111 |
|
| 112 |
| Provider | Description | |
| 113 |
|----------|-------------| |
| 114 |
| `Imagify\User\ServiceProvider` | Registers `User` singleton; binds account/quota service. | |
| 115 |
| `Imagify\Admin\ServiceProvider` | AdminBar, PluginFamily, AdminSubscriber. | |
| 116 |
| `Imagify\Avif\ServiceProvider` | AVIF rewrite-rule writers for Apache/Nginx/IIS. | |
| 117 |
| `Imagify\CDN\ServiceProvider` | CDN push integration. | |
| 118 |
| `Imagify\Picture\ServiceProvider` | Registers `Picture\Display` subscriber for `<picture>` tag rewriting. | |
| 119 |
| `Imagify\Stats\ServiceProvider` | Stat counters (e.g. `OptimizedMediaWithoutNextGen`). | |
| 120 |
| `Imagify\Webp\ServiceProvider` | WebP rewrite-rule writers. | |
| 121 |
| `Imagify\ThirdParty\ServiceProvider` | GravityForms, Extendify; loads all `inc/3rd-party/` integrations. | |
| 122 |
| `Imagify\Media\ServiceProvider` | Media subscribers, upload handler. | |
| 123 |
| `Imagify\Tools\ServiceProvider` | Reset internal state tool, troubleshooting subscriber. | |
| 124 |
|
| 125 |
--- |
| 126 |
|
| 127 |
## 2. Namespace & PSR-4 Structure |
| 128 |
|
| 129 |
*Composer autoload map, directory layout, and naming conventions.* |
| 130 |
|
| 131 |
### PSR-4 Autoload Map (`composer.json`) |
| 132 |
|
| 133 |
| Namespace Prefix | Directory | Notes | |
| 134 |
|-----------------|-----------|-------| |
| 135 |
| `Imagify\` | `classes/` | Primary PSR-4 root for all modern classes | |
| 136 |
| `Imagify\Deprecated\Traits\` | `inc/deprecated/Traits/` | Backward compat trait shims | |
| 137 |
| `Imagify\ThirdParty\AS3CF\` | `inc/3rd-party/amazon-s3-and-cloudfront/classes/` | S3 Offload integration | |
| 138 |
| `Imagify\ThirdParty\EnableMediaReplace\` | `inc/3rd-party/enable-media-replace/classes/` | Enable Media Replace compat | |
| 139 |
| `Imagify\ThirdParty\FormidablePro\` | `inc/3rd-party/formidable-pro/classes/` | Formidable Forms compat | |
| 140 |
| `Imagify\ThirdParty\NGG\` | `inc/3rd-party/nextgen-gallery/classes/` | NextGEN Gallery integration | |
| 141 |
| `Imagify\ThirdParty\RegenerateThumbnails\` | `inc/3rd-party/regenerate-thumbnails/classes/` | Regenerate Thumbnails compat | |
| 142 |
| `Imagify\ThirdParty\WPRocket\` | `inc/3rd-party/wp-rocket/classes/` | WP Rocket compat | |
| 143 |
|
| 144 |
### Classmap (Legacy, Non-PSR-4) |
| 145 |
|
| 146 |
`inc/classes/` and `inc/deprecated/classes/` are loaded via Composer classmap. The convention is `class-imagify-{name}.php` → `Imagify_{Name}`. Two files are explicitly excluded: `class-imagify-plugin.php` and `class-imagify-requirements-check.php` (loaded manually before autoloader is available). |
| 147 |
|
| 148 |
### Key `classes/` Sub-namespaces |
| 149 |
|
| 150 |
| Namespace | Classes | |
| 151 |
|-----------|---------| |
| 152 |
| `Imagify\Bulk\` | `Bulk`, `BulkInterface`, `AbstractBulk`, `WP`, `CustomFolders`, `Noop` | |
| 153 |
| `Imagify\CLI\` | `AbstractCommand`, `BulkOptimizeCommand`, `RestoreCommand`, `GenerateMissingNextgenCommand` | |
| 154 |
| `Imagify\Context\` | `ContextInterface`, `AbstractContext`, `WP`, `CustomFolders`, `Noop` | |
| 155 |
| `Imagify\Media\` | `MediaInterface`, `AbstractMedia`, `WP`, `CustomFolders`, `Noop` | |
| 156 |
| `Imagify\Optimization\` | `File`, `Process\{AbstractProcess, WP, CustomFolders, Noop}`, `Data\{AbstractData, WP, CustomFolders, Noop}` | |
| 157 |
| `Imagify\Picture\` | `Display` (output buffer rewriter) | |
| 158 |
| `Imagify\Job\` | `MediaOptimization` (background queue worker) | |
| 159 |
| `Imagify\Tools\` | `InternalStateList`, `ResetInternalState`, `Subscriber` | |
| 160 |
| `Imagify\Traits\` | `InstanceGetterTrait` (lightweight singleton), `MediaRowTrait` | |
| 161 |
|
| 162 |
> **InstanceGetterTrait** provides `static::get_instance(): static` — a static singleton factory used by both PSR-4 classes and legacy `Imagify_*` classes. It stores the instance in `static::$_instance`. |
| 163 |
|
| 164 |
--- |
| 165 |
|
| 166 |
## 3. Optimization Process — Class Hierarchy & Call Flow |
| 167 |
|
| 168 |
*Full inheritance chain from context factory to per-file API call.* |
| 169 |
|
| 170 |
### Class Inheritance Chain |
| 171 |
|
| 172 |
``` |
| 173 |
ProcessInterface // classes/Optimization/Process/ProcessInterface.php |
| 174 |
└── AbstractProcess // classes/Optimization/Process/AbstractProcess.php (~2100 lines) |
| 175 |
├── Process\WP // WP Media Library context |
| 176 |
├── Process\CustomFolders // Custom Folders context |
| 177 |
└── Process\Noop // No-op fallback |
| 178 |
|
| 179 |
DataInterface // classes/Optimization/Data/DataInterface.php |
| 180 |
└── AbstractData |
| 181 |
├── Data\WP // stores _imagify_data postmeta |
| 182 |
├── Data\CustomFolders // stores in imagify_files table |
| 183 |
└── Data\Noop |
| 184 |
|
| 185 |
MediaInterface // classes/Media/MediaInterface.php |
| 186 |
└── AbstractMedia |
| 187 |
├── Media\WP |
| 188 |
├── Media\CustomFolders |
| 189 |
└── Media\Noop |
| 190 |
|
| 191 |
ContextInterface // classes/Context/ContextInterface.php |
| 192 |
└── AbstractContext |
| 193 |
├── Context\WP |
| 194 |
├── Context\CustomFolders |
| 195 |
└── Context\Noop |
| 196 |
``` |
| 197 |
|
| 198 |
### Context Factory Functions |
| 199 |
|
| 200 |
```php |
| 201 |
// inc/functions/common.php |
| 202 |
imagify_get_context( string $context ): ContextInterface |
| 203 |
imagify_get_optimization_process( int $media_id, string $context ): ProcessInterface |
| 204 |
|
| 205 |
// Context values: 'wp' | 'custom-folders' | 'ngg' (when NGG active) |
| 206 |
// Filterable via: imagify_context_class_name, imagify_process_class_name |
| 207 |
``` |
| 208 |
|
| 209 |
### AbstractProcess — Key Method Signatures |
| 210 |
|
| 211 |
| Method | Signature | Description | |
| 212 |
|--------|-----------|-------------| |
| 213 |
| `__construct` | `(int\|WP_Post\|MediaInterface $id)` | Accepts attachment ID, WP_Post, or MediaInterface object | |
| 214 |
| `optimize` | `(?int $optimization_level, array $args = []): bool\|WP_Error` | Main entry for single-media optimization; acquires lock, iterates sizes | |
| 215 |
| `reoptimize` | `(?int $optimization_level, array $args = []): bool\|WP_Error` | Restore then re-optimize at new level | |
| 216 |
| `optimize_sizes` | `(array $sizes, ?int $level, array $args = []): bool\|WP_Error` | Push sizes to background job queue | |
| 217 |
| `optimize_size` | `(string $size, ?int $level): bool\|WP_Error` | Optimize a single named size (e.g. `'full'`, `'thumbnail'`) | |
| 218 |
| `optimize_missing_thumbnails` | `(): bool\|WP_Error` | Find and optimize sizes missing from postmeta | |
| 219 |
| `restore` | `(): bool\|WP_Error` | Restore all sizes from backup; acquires restoring lock | |
| 220 |
| `delete_backup` | `(): bool\|WP_Error` | Remove backup files for this media | |
| 221 |
| `generate_nextgen_versions` | `(): bool\|WP_Error` | Generate WebP/AVIF variants for all optimized sizes | |
| 222 |
| `delete_nextgen_files` | `(bool $keep_full = false, bool $all_next_gen = false): void` | Remove WebP/AVIF sidecar files | |
| 223 |
| `lock` | `(string $action = 'optimizing'): void` | Set transient lock for 10 minutes | |
| 224 |
| `unlock` | `(): void` | Delete lock transient | |
| 225 |
| `is_locked` | `(): string\|false` | Returns lock action string or false | |
| 226 |
| `update_size_optimization_data` | `(object $response, string $size, int $level): void` | Persist API response data for a size | |
| 227 |
|
| 228 |
### Optimization Call Flow — Single Media |
| 229 |
|
| 230 |
`AbstractProcess::optimize()` → `lock('optimizing')` → `get_sizes_to_optimize()` → `optimize_sizes($sizes, $level)` → `MediaOptimization::push_to_queue()` → `optimize_size($size)` → `File::optimize($args)` → `upload_imagify_image()` → `Imagify API POST /upload/` → `download_url(response->image)` → `filesystem->move()` → `update_size_optimization_data()` → `unlock()` |
| 231 |
|
| 232 |
### Per-Size Data Structure Stored |
| 233 |
|
| 234 |
```php |
| 235 |
// Stored in _imagify_data['sizes'][$size_name] (WP context) |
| 236 |
// On success: |
| 237 |
[ |
| 238 |
'success' => true, |
| 239 |
'original_size' => int, // bytes before optimization |
| 240 |
'optimized_size' => int, // bytes after optimization |
| 241 |
'percent' => float, // savings percentage (2 decimal places) |
| 242 |
] |
| 243 |
|
| 244 |
// On error: |
| 245 |
[ |
| 246 |
'success' => false, |
| 247 |
'error' => string, // human-readable error message |
| 248 |
] |
| 249 |
``` |
| 250 |
|
| 251 |
--- |
| 252 |
|
| 253 |
## 4. Optimization\File — Method Signatures |
| 254 |
|
| 255 |
*Low-level file operations: validation, resize, backup, API call, next-gen path generation.* |
| 256 |
|
| 257 |
Class: `Imagify\Optimization\File` — `classes/Optimization/File.php` (931 lines). |
| 258 |
Injected with `Imagify_Filesystem::get_instance()`. Does not extend anything — purely compositional. |
| 259 |
|
| 260 |
### Constructor & Properties |
| 261 |
|
| 262 |
```php |
| 263 |
class File { |
| 264 |
protected string $path; // absolute path to file |
| 265 |
protected ?bool $is_image; // cached result of is_image() |
| 266 |
protected ?object $file_type; // {ext, type} from wp_check_filetype() |
| 267 |
protected Imagify_Filesystem $filesystem; |
| 268 |
protected mixed $editor; // WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error |
| 269 |
protected array $options; // cached get_imagify_option() calls |
| 270 |
|
| 271 |
public function __construct( string $file_path ) {...} |
| 272 |
} |
| 273 |
``` |
| 274 |
|
| 275 |
### Public Methods |
| 276 |
|
| 277 |
| Method | Parameters → Return | Notes | |
| 278 |
|--------|-------------------|-------| |
| 279 |
| `is_valid()` | `→ bool` | Returns true if `$path` is non-empty | |
| 280 |
| `can_be_processed()` | `→ true\|WP_Error` | Checks: path not empty, filesystem no errors, file exists, is a file, file writable, parent dir writable | |
| 281 |
| `optimize(array $args)` | `→ stdClass\|WP_Error` | Calls backup(), then `upload_imagify_image()`, downloads result, moves to destination | |
| 282 |
| `resize(array $dimensions, int $max_width)` | `→ string\|WP_Error` | Resizes via WP_Image_Editor; corrects EXIF orientation (cases 2–8); returns temp path | |
| 283 |
| `create_thumbnail(array $destination)` | `→ bool\|array\|WP_Error` | Calls `$editor->multi_resize()`; moves to destination path | |
| 284 |
| `backup(?string $backup_path, ?string $backup_source)` | `→ true\|false\|WP_Error` | Copies file to backup_path; also copies `-scaled` variant if exists | |
| 285 |
| `is_exceeded()` | `→ bool` | Returns true if file size > `IMAGIFY_MAX_BYTES` (5 MB) | |
| 286 |
| `is_supported(array $allowed_mime_types)` | `→ bool` | Checks MIME type against allow-list | |
| 287 |
| `is_image()` | `→ bool` | MIME type starts with `image/` | |
| 288 |
| `is_pdf()` | `→ bool` | MIME type is `application/pdf` | |
| 289 |
| `is_webp()` | `→ bool` | Regex: `@(?!^|/|\)\.webp$@i` — rejects bare `.webp` | |
| 290 |
| `is_avif()` | `→ bool` | Same pattern for `.avif` | |
| 291 |
| `get_path()` | `→ string` | Current absolute path (may change post-conversion) | |
| 292 |
| `get_path_to_webp()` | `→ string\|false` | Appends `.webp` to path; false if not an image or already WebP | |
| 293 |
| `get_path_to_nextgen(string $format)` | `→ string\|false` | Appends `.webp` or `.avif`; false if already next-gen | |
| 294 |
| `get_mime_type()` | `→ string` | From cached `wp_check_filetype()` | |
| 295 |
| `get_extension()` | `→ string\|false` | File extension without dot | |
| 296 |
| `get_dimensions()` | `→ array{width:int, height:int}` | Returns `[0,0]` if not image | |
| 297 |
|
| 298 |
### `optimize()` — Args Array |
| 299 |
|
| 300 |
```php |
| 301 |
optimize( [ |
| 302 |
'backup' => true, // false = skip backup regardless of user setting |
| 303 |
'backup_path' => null, // string — explicit backup destination path |
| 304 |
'backup_source' => null, // string — source to backup (WP 5.3+ original) |
| 305 |
'optimization_level' => 0, // 0=normal/lossless, 1=aggressive, 2=ultra |
| 306 |
'convert' => '', // 'webp' | 'avif' | '' for original format |
| 307 |
'context' => 'wp', // sent to API for logging |
| 308 |
'original_size' => 0, // bytes, sent to API |
| 309 |
] ); |
| 310 |
``` |
| 311 |
|
| 312 |
--- |
| 313 |
|
| 314 |
## 5. API Client — Endpoints & Request/Response |
| 315 |
|
| 316 |
*HTTP transport, authentication, all endpoints, response schema, error handling.* |
| 317 |
|
| 318 |
Class: `Imagify` (legacy classmap) — `inc/classes/class-imagify.php`. Singleton via `InstanceGetterTrait`. |
| 319 |
Base URL: `IMAGIFY_APP_API_URL` = `https://app.imagify.io/api/` |
| 320 |
|
| 321 |
### Authentication |
| 322 |
|
| 323 |
```php |
| 324 |
// Headers set in __construct() using stored API key |
| 325 |
$this->all_headers['Accept'] = 'Accept: application/json'; |
| 326 |
$this->all_headers['Content-Type'] = 'Content-Type: application/json'; |
| 327 |
$this->all_headers['Authorization'] = 'Authorization: token ' . $this->api_key; |
| 328 |
|
| 329 |
// upload_image() sends only Authorization header (multipart/form-data via cURL) |
| 330 |
// All other endpoints send all three headers |
| 331 |
``` |
| 332 |
|
| 333 |
### Transport Strategy |
| 334 |
|
| 335 |
The private `http_call()` method auto-selects transport: if `$args['post_data']['image']` is set, it routes to `curl_http_call()` (direct cURL for multipart file uploads); otherwise uses WordPress `wp_remote_request()`. A `pre_imagify_request` filter allows short-circuiting the cURL path. |
| 336 |
|
| 337 |
### All Endpoints |
| 338 |
|
| 339 |
| Method | Endpoint | HTTP | Body / Response | |
| 340 |
|--------|----------|------|----------------| |
| 341 |
| `get_user()` | `users/me/` | GET | JSON: `{id, email, plan_id, plan_label, quota, extra_quota, extra_quota_consumed, consumed_current_month_quota, next_date_update, is_active, is_monthly}` | |
| 342 |
| `create_user($data)` | `users/` | POST | JSON body; no auth header | |
| 343 |
| `update_user($data)` | `users/me/` | PUT | JSON body with all headers | |
| 344 |
| `get_status($data)` | `status/{$data}/` | GET | Cached in static array per type | |
| 345 |
| `get_api_version()` | `version/` | GET | 5s timeout; cached in site transient | |
| 346 |
| `get_public_info()` | `public-info` | GET | Marketing/public plan info | |
| 347 |
| `upload_image($data)` | `upload/` | POST (cURL multipart) | `$data = ['image' => $path, 'data' => json_encode($opts)]`. Response: `{image: $url, ...}` | |
| 348 |
| `fetch_image($data)` | `fetch/` | POST (JSON) | Optimize image from URL; same response shape as upload | |
| 349 |
| `get_plans_prices()` | `pricing/plan/` | GET | Plan pricing objects | |
| 350 |
| `get_all_prices()` | `pricing/all/` | GET | All pricing including packs | |
| 351 |
| `check_coupon_code($coupon)` | `coupons/{$coupon}/` | GET | Coupon validity response | |
| 352 |
| `check_discount()` | `pricing/discount/` | GET | Active discount info | |
| 353 |
|
| 354 |
### Upload Request Body (multipart via cURL) |
| 355 |
|
| 356 |
```php |
| 357 |
// $data array passed to upload_image() |
| 358 |
[ |
| 359 |
'image' => '/absolute/path/to/image.jpg', // CURLFile in cURL transport |
| 360 |
'data' => json_encode([ |
| 361 |
'normal' => true/false, // level === 0 |
| 362 |
'aggressive' => true/false, // level === 1 |
| 363 |
'ultra' => true/false, // level === 2 |
| 364 |
'keep_exif' => true, |
| 365 |
'original_size' => int, |
| 366 |
'context' => string, // 'wp' | 'custom-folders' | 'ngg' |
| 367 |
'convert' => string, // 'webp' | 'avif' — only when converting |
| 368 |
]), |
| 369 |
] |
| 370 |
``` |
| 371 |
|
| 372 |
### API Response Shape (upload/fetch) |
| 373 |
|
| 374 |
```json |
| 375 |
// Success — stdClass |
| 376 |
{ |
| 377 |
"image": "https://app.imagify.io/...temp_url...", |
| 378 |
"original_size": 123456, |
| 379 |
"new_size": 98765, |
| 380 |
"percent": 19.87 |
| 381 |
} |
| 382 |
|
| 383 |
// Error — WP_Error with code 'error {http_code}' |
| 384 |
// HTTP 401 → invalid API key |
| 385 |
// HTTP 413 → file too large |
| 386 |
// HTTP 4xx/5xx → $response->detail or $response->image error array |
| 387 |
``` |
| 388 |
|
| 389 |
### HTTP Response Handling |
| 390 |
|
| 391 |
```php |
| 392 |
private function handle_response( string $response, int $http_code, string $error = '' ) { |
| 393 |
$response = json_decode( $response ); // stdClass or null |
| 394 |
if ( 200 !== $http_code && !empty( $response->code ) ) { |
| 395 |
// $response->detail → WP_Error message |
| 396 |
// $response->image → array of field errors |
| 397 |
return new WP_Error( 'error ' . $http_code, ... ); |
| 398 |
} |
| 399 |
if ( ! is_object( $response ) ) { |
| 400 |
return new WP_Error( 'not_valid_json', ... ); |
| 401 |
} |
| 402 |
return $response; |
| 403 |
} |
| 404 |
``` |
| 405 |
|
| 406 |
> **Timeout defaults:** `get_user()` and `get_status()` use 10s. `get_api_version()` uses 5s. All other calls default to 45s. Filterable via `imagify_api_http_request_timeout`. |
| 407 |
|
| 408 |
--- |
| 409 |
|
| 410 |
## 6. WordPress Postmeta Keys & Data Structures |
| 411 |
|
| 412 |
*Every key written to `wp_postmeta` by Imagify, with types and full schemas.* |
| 413 |
|
| 414 |
### Primary Metadata Keys (WP Media Library) |
| 415 |
|
| 416 |
All stored on the attachment post (`post_type = attachment`). Managed by `Imagify\Optimization\Data\WP`. |
| 417 |
|
| 418 |
| Meta Key | Type | Values / Schema | |
| 419 |
|----------|------|----------------| |
| 420 |
| `_imagify_data` | Serialized array | `['sizes' => [...], 'stats' => [...], 'message' => string]` | |
| 421 |
| `_imagify_status` | string | `'success'` \| `'already_optimized'` \| error string \| `''` (not optimized) | |
| 422 |
| `_imagify_optimization_level` | int (stored as string) | `0` = normal/lossless, `1` = aggressive, `2` = ultra/smart | |
| 423 |
|
| 424 |
### `_imagify_data` Full Schema |
| 425 |
|
| 426 |
```php |
| 427 |
// _imagify_data serialized array structure |
| 428 |
[ |
| 429 |
'sizes' => [ |
| 430 |
'full' => [ 'success' => true, 'original_size' => int, 'optimized_size' => int, 'percent' => float ], |
| 431 |
'thumbnail' => [ 'success' => true, ... ], |
| 432 |
'medium' => [ 'success' => false, 'error' => string ], |
| 433 |
// ... one entry per registered image size + any custom sizes |
| 434 |
], |
| 435 |
'stats' => [ |
| 436 |
'original_size' => int, // sum across all successful sizes |
| 437 |
'optimized_size' => int, // sum across all successful sizes |
| 438 |
'percent' => float, // aggregate % (2 decimal places) |
| 439 |
], |
| 440 |
'message' => string, // optional message from API (e.g. already optimized) |
| 441 |
] |
| 442 |
``` |
| 443 |
|
| 444 |
> `_imagify_status` and `_imagify_optimization_level` are written only when the `'full'` size is updated. They act as top-level fast-access keys mirroring `_imagify_data['sizes']['full']`. |
| 445 |
|
| 446 |
### Standard WordPress Keys (also modified by Imagify) |
| 447 |
|
| 448 |
| Meta Key | Modified When | |
| 449 |
|----------|-------------| |
| 450 |
| `_wp_attachment_metadata` | After resize (adds/removes sizes array entries); after thumbnail generation (updates width/height); after WP 5.3 original file handling | |
| 451 |
| `_wp_attached_file` | Not directly modified; read to resolve absolute paths | |
| 452 |
|
| 453 |
--- |
| 454 |
|
| 455 |
## 7. Settings — Option Keys, Types & Defaults |
| 456 |
|
| 457 |
*All values stored under a single serialized option. Class: `Imagify_Options` (`inc/classes/class-imagify-options.php`).* |
| 458 |
|
| 459 |
Option name: `imagify_settings` (single site) or `imagify_settings` stored via `get_site_option` (network). Set via `get_imagify_option($key)` / `update_imagify_option($key, $value)`. |
| 460 |
|
| 461 |
| Key | Type | Default | Reset Value | Description | |
| 462 |
|-----|------|---------|-------------|-------------| |
| 463 |
| `api_key` | string | `''` | — | Imagify API key. Overridable via PHP constant `IMAGIFY_API_KEY`. | |
| 464 |
| `optimization_level` | int | `2` | `2` | 0=lossless, 1=aggressive, 2=ultra | |
| 465 |
| `lossless` | int (bool) | `0` | — | Force level 0 for all optimizations | |
| 466 |
| `auto_optimize` | int (bool) | `0` | `1` | Auto-optimize on upload | |
| 467 |
| `backup` | int (bool) | `0` | `1` | Keep backup of originals | |
| 468 |
| `resize_larger` | int (bool) | `0` | `1` (if WP 5.3+) | Resize images larger than threshold | |
| 469 |
| `resize_larger_w` | int | `0` | From `big_image_size_threshold` filter (default 2560) | Max width in pixels for resize | |
| 470 |
| `display_nextgen` | int (bool) | `0` | — | Enable next-gen format delivery | |
| 471 |
| `display_nextgen_method` | string | `'picture'` | — | `'picture'` = HTML rewrite; `'rewrite'` = server-side rules | |
| 472 |
| `display_webp` | int (bool) | `0` | — | Legacy WebP delivery toggle | |
| 473 |
| `display_webp_method` | string | `'picture'` | — | Legacy WebP method selector | |
| 474 |
| `cdn_url` | string | `''` | — | CDN base URL for URL→path resolution | |
| 475 |
| `disallowed-sizes` | array | `[]` | — | Size names excluded from optimization | |
| 476 |
| `admin_bar_menu` | int (bool) | `1` | `1` | Show Imagify in admin bar | |
| 477 |
| `partner_links` | int (bool) | `0` | `1` | Show partner links in plugin UI | |
| 478 |
| `convert_to_avif` | int (bool) | `0` | — | Generate AVIF sidecar files | |
| 479 |
| `convert_to_webp` | int (bool) | `0` | — | Generate WebP sidecar files | |
| 480 |
| `optimization_format` | string | `'webp'` | — | `'webp'` \| `'avif'` \| `'off'` | |
| 481 |
|
| 482 |
> The `reset_values` array in `Imagify_Options` contains only keys that differ from defaults; it is applied on first install or explicit reset. The option is a single serialized blob — never stored as individual keys. |
| 483 |
|
| 484 |
--- |
| 485 |
|
| 486 |
## 8. Database Schemas |
| 487 |
|
| 488 |
*Complete DDL for all three custom tables: `imagify_folders`, `imagify_files`, and `ngg_imagify_data`.* |
| 489 |
|
| 490 |
### `imagify_folders` |
| 491 |
|
| 492 |
Class: `Imagify_Folders_DB` (`inc/classes/class-imagify-folders-db.php`). Global table in multisite (`$wpdb->base_prefix`). Table version: `100`. |
| 493 |
|
| 494 |
```sql |
| 495 |
CREATE TABLE `{prefix}imagify_folders` ( |
| 496 |
`folder_id` bigint(20) unsigned NOT NULL auto_increment, |
| 497 |
`path` varchar(191) NOT NULL default '', |
| 498 |
`active` tinyint(1) unsigned NOT NULL default 0, |
| 499 |
PRIMARY KEY (folder_id), |
| 500 |
UNIQUE KEY path (path), |
| 501 |
KEY active (active) |
| 502 |
); |
| 503 |
``` |
| 504 |
|
| 505 |
| Column | Type | Description | |
| 506 |
|--------|------|-------------| |
| 507 |
| `folder_id` | bigint unsigned PK | Auto-increment primary key | |
| 508 |
| `path` | varchar(191) UNIQUE | Absolute path with placeholder: `{{ROOT}}/wp-content/uploads/gallery/`. Uses `{{ROOT}}` and `{{ABSPATH}}` tokens for portability. | |
| 509 |
| `active` | tinyint(1) | `1` = selected in settings; `0` = deactivated. Indexed for fast active-folder queries. | |
| 510 |
|
| 511 |
### `imagify_files` |
| 512 |
|
| 513 |
Class: `Imagify_Files_DB` (`inc/classes/class-imagify-files-db.php`). Global table in multisite. Table version: `102`. |
| 514 |
|
| 515 |
```sql |
| 516 |
CREATE TABLE `{prefix}imagify_files` ( |
| 517 |
`file_id` bigint(20) unsigned NOT NULL auto_increment, |
| 518 |
`folder_id` bigint(20) unsigned NOT NULL default 0, |
| 519 |
`file_date` datetime NOT NULL default '0000-00-00 00:00:00', |
| 520 |
`path` varchar(191) NOT NULL default '', |
| 521 |
`hash` varchar(32) NOT NULL default '', -- MD5 of file |
| 522 |
`mime_type` varchar(100) NOT NULL default '', |
| 523 |
`modified` tinyint(1) unsigned NOT NULL default 0, |
| 524 |
`width` smallint(2) unsigned NOT NULL default 0, |
| 525 |
`height` smallint(2) unsigned NOT NULL default 0, |
| 526 |
`original_size` int(4) unsigned NOT NULL default 0, |
| 527 |
`optimized_size` int(4) unsigned default NULL, |
| 528 |
`percent` smallint(2) unsigned default NULL, |
| 529 |
`optimization_level` tinyint(1) unsigned default NULL, |
| 530 |
`status` varchar(20) default NULL, |
| 531 |
`error` varchar(255) default NULL, |
| 532 |
`data` longtext default NULL, -- serialized |
| 533 |
PRIMARY KEY (file_id), |
| 534 |
UNIQUE KEY path (path), |
| 535 |
KEY folder_id (folder_id), |
| 536 |
KEY optimization_level (optimization_level), |
| 537 |
KEY status (status), |
| 538 |
KEY modified (modified) |
| 539 |
); |
| 540 |
``` |
| 541 |
|
| 542 |
| Column | Notes | |
| 543 |
|--------|-------| |
| 544 |
| `folder_id` | FK reference to `imagify_folders.folder_id` (not enforced at DB level) | |
| 545 |
| `path` | Absolute path using same `{{ROOT}}` tokens as folders table | |
| 546 |
| `hash` | MD5 hash of file contents — used by `refresh_file()` to detect modifications | |
| 547 |
| `modified` | `1` when file has changed since last optimization (hash mismatch) | |
| 548 |
| `status` | `'success'` \| `'already_optimized'` \| `'error'` \| NULL (not yet processed) | |
| 549 |
| `data` | Serialized array — same shape as `_imagify_data` (sizes + stats) | |
| 550 |
|
| 551 |
### `ngg_imagify_data` (NextGEN Gallery) |
| 552 |
|
| 553 |
Class: `Imagify\ThirdParty\NGG\DB` (`inc/3rd-party/nextgen-gallery/classes/DB.php`). Per-site table (`$wpdb->prefix`). Table version: `100`. |
| 554 |
|
| 555 |
```sql |
| 556 |
CREATE TABLE `{prefix}ngg_imagify_data` ( |
| 557 |
`data_id` bigint(20) unsigned NOT NULL auto_increment, |
| 558 |
`pid` bigint(20) unsigned NOT NULL default 0, |
| 559 |
`optimization_level` varchar(1) NOT NULL default '', |
| 560 |
`status` varchar(30) NOT NULL default '', |
| 561 |
`data` longtext default NULL, |
| 562 |
PRIMARY KEY (data_id), |
| 563 |
KEY pid (pid) |
| 564 |
); |
| 565 |
``` |
| 566 |
|
| 567 |
`pid` is the NextGEN picture ID. `data` is serialized the same way as `_imagify_data`. |
| 568 |
|
| 569 |
### Abstract DB Base Class |
| 570 |
|
| 571 |
All three DB classes extend `Imagify_Abstract_DB` which implements `Imagify\DB\DBInterface`. It provides: |
| 572 |
|
| 573 |
**Table Management** |
| 574 |
- `maybe_upgrade_table()` — create/upgrade on plugin init |
| 575 |
- `create_table()` — issues `dbDelta()` |
| 576 |
- `can_operate(): bool` — true when table is ready |
| 577 |
- Version stored in option: `{option_prefix}_db_version` |
| 578 |
|
| 579 |
**CRUD Methods** |
| 580 |
- `get($id)`, `get_by($col, $val)`, `get_in($col, $vals)` |
| 581 |
- `get_var($col, $where)`, `get_column_in($col, $ids)` |
| 582 |
- `insert($data)`, `update($data, $where)`, `delete($id)` |
| 583 |
- Auto-serialize array columns before insert/update via `serialize_columns()` |
| 584 |
- Auto-cast results via `cast_row()` based on column type map |
| 585 |
|
| 586 |
--- |
| 587 |
|
| 588 |
## 9. Bulk Optimization — ActionScheduler Integration |
| 589 |
|
| 590 |
*How bulk jobs are enqueued, tracked, and completed via ActionScheduler async actions.* |
| 591 |
|
| 592 |
Class: `Imagify\Bulk\Bulk` (`classes/Bulk/Bulk.php`). Singleton. Registered hooks in `init()`. |
| 593 |
|
| 594 |
### Bulk Run Flow |
| 595 |
|
| 596 |
`AJAX: imagify_bulk_optimize` → `bulk_optimize_callback()` → `run_optimize($context, $level)` → `get_unoptimized_media_ids()` → `as_enqueue_async_action() ×N` → `set_transient 'running'` → `ActionScheduler fires 'imagify_optimize_media'` → `optimize_media($id, $ctx, $lvl)` → `check_optimization_status()` |
| 597 |
|
| 598 |
### ActionScheduler Job Enqueue |
| 599 |
|
| 600 |
```php |
| 601 |
// Bulk::run_optimize() — one as_enqueue_async_action() per media |
| 602 |
as_enqueue_async_action( |
| 603 |
'imagify_optimize_media', |
| 604 |
[ |
| 605 |
'id' => (int) $media_id, |
| 606 |
'context' => (string) $context, // 'wp' | 'custom-folders' |
| 607 |
'level' => (int) $optimization_level, |
| 608 |
], |
| 609 |
"imagify-{$context}-optimize-media" // group name — allows cancellation per context |
| 610 |
); |
| 611 |
|
| 612 |
// Next-gen generation uses a separate hook |
| 613 |
as_enqueue_async_action( |
| 614 |
'imagify_convert_next_gen', |
| 615 |
[ 'id' => $media_id, 'context' => $context ], |
| 616 |
"imagify-{$context}-convert-nextgen" |
| 617 |
); |
| 618 |
``` |
| 619 |
|
| 620 |
### Progress Tracking Transients |
| 621 |
|
| 622 |
| Transient | Set When | Shape | TTL | |
| 623 |
|-----------|---------|-------|-----| |
| 624 |
| `imagify_wp_optimize_running` | Start of WP library bulk run | `['total' => int, 'remaining' => int]` | DAY_IN_SECONDS | |
| 625 |
| `imagify_custom-folders_optimize_running` | Start of custom-folders bulk run | `['total' => int, 'remaining' => int]` | DAY_IN_SECONDS | |
| 626 |
| `imagify_bulk_optimization_result` | After each successful optimization | `['total' => int, 'original_size' => int, 'optimized_size' => int]` | DAY_IN_SECONDS | |
| 627 |
| `imagify_bulk_optimization_complete` | When remaining reaches 0 | `1` | DAY_IN_SECONDS | |
| 628 |
| `imagify_missing_next_gen_total` | Start of next-gen generation run | `int` (total count) | HOUR_IN_SECONDS | |
| 629 |
| `imagify_bulk_optimization_infos` | User dismisses info popup | `1` | WEEK_IN_SECONDS | |
| 630 |
|
| 631 |
### ActionScheduler Job Lifecycle |
| 632 |
|
| 633 |
ActionScheduler is bundled at `inc/Dependencies/ActionScheduler/action-scheduler.php`. Jobs go through: `pending` → `in-progress` → `complete` |
| 634 |
|
| 635 |
On failure: **failed**. On cancel: **canceled**. The `check_optimization_status()` hook fires on `imagify_after_optimize` and decrements the running counter, deleting the transient and setting the complete transient when all jobs finish. |
| 636 |
|
| 637 |
### Multisite Context Routing |
| 638 |
|
| 639 |
```php |
| 640 |
// Bulk::get_contexts() — determines which contexts appear on bulk page |
| 641 |
if ( ! is_network_admin() ) { |
| 642 |
$types['library|wp'] = 1; // library only in site admin |
| 643 |
} |
| 644 |
if ( imagify_is_active_for_network() && is_network_admin() ) { |
| 645 |
$types['custom-folders|custom-folders'] = 1; // custom folders in network admin |
| 646 |
} elseif ( ! imagify_is_active_for_network() ) { |
| 647 |
$types['custom-folders|custom-folders'] = 1; // custom folders in site admin |
| 648 |
} |
| 649 |
``` |
| 650 |
|
| 651 |
--- |
| 652 |
|
| 653 |
## 10. Concurrency & Locking Mechanisms |
| 654 |
|
| 655 |
*Transient-based per-media locks that prevent duplicate concurrent optimization or restore jobs.* |
| 656 |
|
| 657 |
### Per-Media Process Lock |
| 658 |
|
| 659 |
Defined in `AbstractProcess`. Each lock is a transient named after the context and media ID. |
| 660 |
|
| 661 |
```php |
| 662 |
// Transient name pattern (LOCK_NAME constant) |
| 663 |
const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked'; |
| 664 |
// Example: 'imagify_wp_42_process_locked' |
| 665 |
// Example: 'imagify_custom-folders_7_process_locked' |
| 666 |
|
| 667 |
// Network-aware: uses set_site_transient when context is_network_wide() |
| 668 |
public function lock( string $action = 'optimizing' ): void { |
| 669 |
$name = $this->get_lock_name(); // sprintf(LOCK_NAME, ctx, id) |
| 670 |
$callback = $media->get_context_instance()->is_network_wide() |
| 671 |
? 'set_site_transient' : 'set_transient'; |
| 672 |
call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS ); |
| 673 |
} |
| 674 |
|
| 675 |
public function is_locked(): string|false { |
| 676 |
// Returns 'optimizing' | 'restoring' | false |
| 677 |
$callback = ...'get_site_transient' or 'get_transient'...; |
| 678 |
$action = call_user_func( $callback, $name ); |
| 679 |
return $this->validate_lock_action( $action ); // normalizes 'restore' → 'restoring' |
| 680 |
} |
| 681 |
|
| 682 |
public function unlock(): void { |
| 683 |
$callback = ...'delete_site_transient' or 'delete_transient'...; |
| 684 |
call_user_func( $callback, $name ); |
| 685 |
} |
| 686 |
``` |
| 687 |
|
| 688 |
### Lock Actions |
| 689 |
|
| 690 |
| Value | Set By | Cleared By | |
| 691 |
|-------|--------|-----------| |
| 692 |
| `'optimizing'` | `optimize()` before iterating sizes | `optimize()` after all sizes complete | |
| 693 |
| `'restoring'` | `restore()` at start | `restore()` at end (success or error) | |
| 694 |
|
| 695 |
### Transient Name Summary for Cleanup |
| 696 |
|
| 697 |
``` |
| 698 |
'_transient_%imagify-auto-optimize-%' // Legacy (deprecated) |
| 699 |
'_transient_%imagify_rpc_%' // Legacy (deprecated) |
| 700 |
'_transient_imagify_%_process_locked' // Active single-site locks |
| 701 |
'_site_transient_imagify_%_process_lock%' // Active network-wide locks |
| 702 |
``` |
| 703 |
|
| 704 |
> **TTL is 10 minutes.** If a PHP process dies mid-optimization, the lock expires automatically. The `Reset Internal State` admin tool can force-clear all locks immediately via direct SQL DELETE. |
| 705 |
|
| 706 |
--- |
| 707 |
|
| 708 |
## 11. Picture\Display — Output Buffer HTML Rewrite |
| 709 |
|
| 710 |
*How `<img>` tags are rewritten to `<picture>` tags at the HTTP response level.* |
| 711 |
|
| 712 |
Class: `Imagify\Picture\Display` (`classes/Picture/Display.php`). Implements `SubscriberInterface`. |
| 713 |
|
| 714 |
### Subscribed Events |
| 715 |
|
| 716 |
```php |
| 717 |
public static function get_subscribed_events(): array { |
| 718 |
return [ |
| 719 |
'template_redirect' => 'start_content_process', |
| 720 |
'imagify_process_webp_content' => 'process_content', |
| 721 |
]; |
| 722 |
} |
| 723 |
``` |
| 724 |
|
| 725 |
### Full Rewrite Pipeline |
| 726 |
|
| 727 |
`template_redirect` → `start_content_process()` → `ob_start([this, 'maybe_process_buffer'])` → `PHP renders full page HTML` → `maybe_process_buffer($buffer)` → `is_html() check (must contain </html> and be >255 chars)` → `process_content($buffer)` → `remove_picture_tags() — strip existing <picture> wrappers` → `get_images() — regex extract all <img> tags` → `process_image() per tag` → `filesystem->exists() check for .webp/.avif sidecars` → `build_picture_tag() → str_replace() in buffer` |
| 728 |
|
| 729 |
### Guards in `start_content_process()` |
| 730 |
|
| 731 |
- `get_imagify_option('display_nextgen')` must be truthy |
| 732 |
- `get_imagify_option('display_nextgen_method')` must equal `'picture'` (`Display::OPTION_VALUE`) |
| 733 |
- Filter `imagify_allow_picture_tags_for_nextgen` must return true |
| 734 |
|
| 735 |
### Lazy-Load Support |
| 736 |
|
| 737 |
The parser checks these src attributes in priority order: `data-lazy-src` → `data-src` → `src`. Likewise for srcset: `data-lazy-srcset` → `data-srcset` → `srcset`. The generated `<source>` tag mirrors whichever attribute was active. |
| 738 |
|
| 739 |
### Generated HTML Structure |
| 740 |
|
| 741 |
```html |
| 742 |
<!-- Input --> |
| 743 |
<img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> |
| 744 |
|
| 745 |
<!-- Output (when both AVIF and WebP exist) --> |
| 746 |
<picture> |
| 747 |
<source type="image/avif" srcset="/uploads/photo.jpg.avif, /uploads/photo-300.jpg.avif 300w" sizes="..."> |
| 748 |
<source type="image/webp" srcset="/uploads/photo.jpg.webp, /uploads/photo-300.jpg.webp 300w" sizes="..."> |
| 749 |
<img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> |
| 750 |
</picture> |
| 751 |
``` |
| 752 |
|
| 753 |
### URL → Path Resolution |
| 754 |
|
| 755 |
`url_to_path()` converts image URLs to filesystem paths for existence checks. It handles: uploads URL, site root URL, CDN URL (via `imagify_cdn_source_url` filter), and protocol-relative URLs. Static caches are maintained per request. |
| 756 |
|
| 757 |
### Filters on the Rewrite Path |
| 758 |
|
| 759 |
| Filter | Signature | Purpose | |
| 760 |
|--------|-----------|---------| |
| 761 |
| `imagify_allow_picture_tags_for_nextgen` | `(bool $allow): bool` | Global on/off switch for the rewriter | |
| 762 |
| `imagify_webp_picture_images_to_display` | `(array $images, string $content): array` | Filter/add/remove images before rewriting | |
| 763 |
| `imagify_webp_picture_process_image` | `(array $data, string $img_tag): array\|false` | Per-image data manipulation (used by S3 Offload integration) | |
| 764 |
| `imagify_picture_attributes` | `(array $attributes, array $data): array` | Attributes on the `<picture>` element | |
| 765 |
| `imagify_picture_source_attributes` | `(array $attributes, array $data): array` | Attributes on each `<source>` element | |
| 766 |
| `imagify_picture_img_attributes` | `(array $attributes, array $data): array` | Attributes on the fallback `<img>` | |
| 767 |
| `imagify_additional_source_tags` | `(string $html, array $data): string` | Inject extra `<source>` elements before the generated ones | |
| 768 |
| `imagify_buffer` | `(string $buffer): string` | Final buffer after all replacements | |
| 769 |
| `imagify_cdn_source_url` | `(string $url): string` | CDN base URL for URL-to-path mapping | |
| 770 |
|
| 771 |
--- |
| 772 |
|
| 773 |
## 12. AJAX & Admin-Post — Full Security Table |
| 774 |
|
| 775 |
*Every `wp_ajax_*` and `admin_post_*` action with its nonce name and capability requirement.* |
| 776 |
|
| 777 |
Security is enforced by `imagify_check_nonce($action, $query_arg)` which wraps `check_ajax_referer()` and calls `imagify_die()` on failure. Capability checks use `imagify_get_context($ctx)->current_user_can($capability, $media_id)`. |
| 778 |
|
| 779 |
### `wp_ajax_*` + `admin_post_*` (both) |
| 780 |
|
| 781 |
| Action | Nonce Name | Capability | Description | |
| 782 |
|--------|-----------|-----------|-------------| |
| 783 |
| `imagify_manual_optimize` | `imagify-optimize-{id}-{ctx}` | `manual-optimize` | Optimize single attachment | |
| 784 |
| `imagify_manual_reoptimize` | `imagify-manual-reoptimize-{id}-{ctx}` | `manual-optimize` | Re-optimize at different level | |
| 785 |
| `imagify_optimize_missing_sizes` | `imagify-optimize-missing-sizes-{id}-{ctx}` | `manual-optimize` | Generate missing thumbnail sizes | |
| 786 |
| `imagify_generate_nextgen_versions` | `imagify-generate-nextgen-versions-{id}-{ctx}` | `manual-optimize` | Generate WebP/AVIF for one attachment | |
| 787 |
| `imagify_delete_nextgen_versions` | `imagify-delete-nextgen-versions-{id}-{ctx}` | `manual-restore` | Remove WebP/AVIF sidecar files | |
| 788 |
| `imagify_restore` | `imagify-restore-{id}-{ctx}` | `manual-restore` | Restore attachment from backup | |
| 789 |
| `imagify_optimize_file` | `imagify_optimize_file` | `manual-optimize` (custom-folders ctx) | Optimize custom folder file | |
| 790 |
| `imagify_reoptimize_file` | `imagify_reoptimize_file` | `manual-optimize` (custom-folders ctx) | Re-optimize custom folder file | |
| 791 |
| `imagify_restore_file` | `imagify_restore_file` | `manual-restore` (custom-folders ctx) | Restore custom folder file from backup | |
| 792 |
| `imagify_refresh_file_modified` | `imagify_refresh_file_modified` | `manual-optimize` (custom-folders ctx) | Refresh file hash/modified status | |
| 793 |
|
| 794 |
### `wp_ajax_*` Only |
| 795 |
|
| 796 |
| Action | Nonce Name | Capability / Check | Description | |
| 797 |
|--------|-----------|-------------------|-------------| |
| 798 |
| `imagify_bulk_optimize` | `imagify-bulk-optimize` | `bulk-optimize` | Launch ActionScheduler bulk job | |
| 799 |
| `imagify_missing_nextgen_generation` | `imagify-bulk-optimize` | `bulk-optimize` per context | Generate all missing next-gen files | |
| 800 |
| `imagify_get_folder_type_data` | `imagify-bulk-optimize` | `bulk-optimize` | Stats for one folder type on bulk page | |
| 801 |
| `imagify_bulk_info_seen` | `imagify-bulk-optimize` | `bulk-optimize` | Set `imagify_bulk_optimization_infos` transient | |
| 802 |
| `imagify_bulk_get_stats` | `imagify-bulk-optimize` | `bulk-optimize` per folder type | Aggregate bulk page statistics | |
| 803 |
| `imagify_reset_internal_state` | `imagify_reset_internal_state` | `manage` (wp ctx) | Clear all locks, transients, AS jobs | |
| 804 |
| `imagify_check_backup_dir_is_writable` | `imagify_check_backup_dir_is_writable` | `manage` (wp ctx) | Test backup directory writability | |
| 805 |
| `imagify_get_files_tree` | `get-files-tree` | `manage` (custom-folders ctx) | Filesystem tree for folder picker | |
| 806 |
| `imagify_signup` | `imagify-signup` | `manage` (wp ctx) | Create Imagify account | |
| 807 |
| `imagify_check_api_key_validity` | `imagify-check-api-key` | `manage` | Validate API key against API | |
| 808 |
| `imagify_get_prices` | `imagify_get_pricing_{user_id}` | `manage` | Fetch plan prices | |
| 809 |
| `imagify_check_coupon` | `imagify_get_pricing_{user_id}` | `manage` | Validate coupon code | |
| 810 |
| `imagify_get_discount` | `imagify_get_pricing_{user_id}` | `manage` | Check active discount | |
| 811 |
| `imagify_get_images_counts` | `imagify_get_pricing_{user_id}` | `manage` | Count images per status | |
| 812 |
| `imagify_update_estimate_sizes` | `update_estimate_sizes` | `manage` | Recalculate size estimates | |
| 813 |
| `imagify_get_user_data` | `imagify_get_user_data` | `manage` | Fetch fresh account data from API | |
| 814 |
| `imagify_delete_user_data_cache` | `imagify_delete_user_data_cache` | `manage` | Purge cached user data transient | |
| 815 |
| `nopriv_imagify_rpc` | `imagify_rpc_{rpc_id}` | None (nonce only) | Internal RPC dispatch | |
| 816 |
|
| 817 |
### `admin_post_*` Only |
| 818 |
|
| 819 |
| Action | Nonce Name | Capability | |
| 820 |
|--------|-----------|-----------| |
| 821 |
| `imagify_scan_custom_folders` | `imagify_scan_custom_folders` | `optimize` (custom-folders ctx) | |
| 822 |
| `imagify_dismiss_ad` | `imagify-dismiss-ad` | `manage` (wp ctx) | |
| 823 |
| `imagify_dismiss_notice` | `imagify-dismiss-notice` | Varies per notice | |
| 824 |
| `imagify_deactivate_plugin` | `imagify-deactivate-plugin` | Varies per notice | |
| 825 |
| `imagify_rollback` | `imagify_rollback` | `manage_options` | |
| 826 |
|
| 827 |
> Nonces with `{id}` and `{ctx}` are unique per media item and context (e.g. `imagify-optimize-42-wp`). This prevents CSRF replay across different media items. |
| 828 |
|
| 829 |
--- |
| 830 |
|
| 831 |
## 13. WP-CLI Commands — Full Signatures |
| 832 |
|
| 833 |
See section 9 in [imagify-summary.md](imagify-summary.md) for usage details. |
| 834 |
|
| 835 |
### `wp imagify bulk-optimize` |
| 836 |
- **Context:** `wp`, `custom-folders` (default: `wp`) |
| 837 |
- **Options:** `--optimization-level=<0|1|2>` |
| 838 |
- **Execution:** Asynchronous via ActionScheduler |
| 839 |
|
| 840 |
### `wp imagify restore` |
| 841 |
- **Context:** `library`, `custom-folders` |
| 842 |
- **Execution:** Synchronous (blocking) |
| 843 |
- **Returns:** success count, error count, total |
| 844 |
|
| 845 |
### `wp imagify generate-missing-nextgen` |
| 846 |
- **Context:** all optimized images across contexts |
| 847 |
- **Execution:** Asynchronous via ActionScheduler |
| 848 |
|
| 849 |
--- |
| 850 |
|
| 851 |
## 14. Developer Hooks — Exact Signatures & Parameter Types |
| 852 |
|
| 853 |
See section 13 in [imagify-summary.md](imagify-summary.md) for the full table. Below are additional filter signatures from the Picture\Display system. |
| 854 |
|
| 855 |
| Filter | Full Signature | Notes | |
| 856 |
|--------|---------------|-------| |
| 857 |
| `imagify_allow_picture_tags_for_nextgen` | `apply_filters('imagify_allow_picture_tags_for_nextgen', bool $allow)` | Return `false` to disable `<picture>` rewriting globally | |
| 858 |
| `imagify_picture_attributes` | `apply_filters('imagify_picture_attributes', array $attr, array $data)` | `$data` contains `src`, `srcset`, `sizes`, `img_tag` | |
| 859 |
| `imagify_buffer` | `apply_filters('imagify_buffer', string $html)` | Full page HTML after all replacements | |
| 860 |
| `imagify_register_context` | `apply_filters('imagify_register_context', array $contexts)` | Register a custom context; key = context slug, value = class name | |
| 861 |
| `imagify_backup_directory` | `apply_filters('imagify_backup_directory', string $path, int $attachment_id)` | Override per-attachment backup directory | |
| 862 |
| `imagify_api_http_request_timeout` | `apply_filters('imagify_api_http_request_timeout', int $timeout, string $endpoint)` | Override API request timeout in seconds | |
| 863 |
| `imagify_event_recurrence` | `apply_filters('imagify_event_recurrence', string $recurrence, string $event)` | Change cron job recurrence (e.g. `'daily'`, `'hourly'`) | |
| 864 |
| `imagify_site_root_url` | `apply_filters('imagify_site_root_url', string $root_url, int $blog_id)` | Override the site root URL used for internal-URL matching. Needed on multisites with domain mapping | |
| 865 |
|
| 866 |
--- |
| 867 |
|
| 868 |
## 15. Scheduled Tasks — Cron & ActionScheduler |
| 869 |
|
| 870 |
See section 12 in [imagify-summary.md](imagify-summary.md) for the cron table. |
| 871 |
|
| 872 |
### ActionScheduler Table |
| 873 |
|
| 874 |
ActionScheduler stores its job queue in `{prefix}actionscheduler_actions`. Key columns relevant to Imagify: |
| 875 |
|
| 876 |
| Column | Notes | |
| 877 |
|--------|-------| |
| 878 |
| `hook` | `'imagify_optimize_media'` or `'imagify_convert_next_gen'` | |
| 879 |
| `group` | `'imagify-wp-optimize-media'`, `'imagify-custom-folders-optimize-media'`, etc. | |
| 880 |
| `args` | JSON: `{"id": 42, "context": "wp", "level": 2}` | |
| 881 |
| `status` | `'pending'` \| `'in-progress'` \| `'complete'` \| `'failed'` \| `'canceled'` | |
| 882 |
|
| 883 |
All Imagify AS jobs are **async actions** (`as_enqueue_async_action()`), not scheduled recurring jobs. |
| 884 |
|
| 885 |
--- |
| 886 |
|
| 887 |
## 16. Multisite Handling |
| 888 |
|
| 889 |
- Plugin can be network-activated (`imagify_is_active_for_network()` → true). |
| 890 |
- When network-activated, settings are stored in `wp_sitemeta` via `get_site_option`. |
| 891 |
- `imagify_folders` and `imagify_files` tables use `$wpdb->base_prefix` (global tables shared across sites). |
| 892 |
- `ngg_imagify_data` uses `$wpdb->prefix` (per-site). |
| 893 |
- Locks use `set_site_transient` when `$context->is_network_wide()` is true. |
| 894 |
- Bulk page shows `custom-folders` context in network admin; `library` context only in site admin. |
| 895 |
- Quota is shared across all sites on the same API key. |
| 896 |
|
| 897 |
--- |
| 898 |
|
| 899 |
## 17. NextGEN Gallery Integration |
| 900 |
|
| 901 |
Automatically detected via `class_exists('C_Gallery_Storage')`. Activates `Imagify\Context\NGG` and `Imagify\ThirdParty\NGG\DB`. |
| 902 |
|
| 903 |
**Compatibility check:** `imagify_ngg_has_pope_storage()` verifies NGG v4.x is present before activating deep integration. |
| 904 |
|
| 905 |
**Data storage:** `{prefix}ngg_imagify_data` (per-site, see schema in section 8). |
| 906 |
|
| 907 |
**Bulk context:** registered as `'ngg'`. Appears on the bulk page when NextGEN Gallery is active. |
| 908 |
|
| 909 |
--- |
| 910 |
|
| 911 |
## 18. Third-Party Integrations |
| 912 |
|
| 913 |
All loaded by `Imagify\ThirdParty\ServiceProvider` which scans `inc/3rd-party/` on boot. |
| 914 |
|
| 915 |
| Plugin | Integration Path | What it does | |
| 916 |
|--------|-----------------|-------------| |
| 917 |
| WooCommerce | `inc/3rd-party/woocommerce/` | Fixes `wp-post-image` class on `<picture>` tags for product image switching | |
| 918 |
| WP Rocket | `inc/3rd-party/wp-rocket/classes/` | PSR-4 compat shim | |
| 919 |
| Gravity Forms | `Imagify\ThirdParty\ServiceProvider` | Registers Gravity Forms upload folder as custom folder context | |
| 920 |
| Formidable Pro | `inc/3rd-party/formidable-pro/classes/` | Same approach as Gravity Forms | |
| 921 |
| Yoast SEO | `inc/3rd-party/` | Ensures Open Graph images are optimized | |
| 922 |
| AMP | `inc/3rd-party/` | Disables `<picture>` rewriter on AMP pages | |
| 923 |
| Enable Media Replace | `inc/3rd-party/enable-media-replace/classes/` | Re-optimizes on media replacement | |
| 924 |
| Regenerate Thumbnails | `inc/3rd-party/regenerate-thumbnails/classes/` | Re-triggers optimization after thumbnail regeneration | |
| 925 |
| Amazon S3 / CloudFront | `inc/3rd-party/amazon-s3-and-cloudfront/classes/` | S3 URL resolution for `url_to_path()` | |
| 926 |
| Cloudflare Super Page Cache | `inc/3rd-party/` | Cache purge on next-gen file generation | |
| 927 |
|
| 928 |
--- |
| 929 |
|
| 930 |
## 19. Quota & Account Management |
| 931 |
|
| 932 |
See section 14 in [imagify-summary.md](imagify-summary.md). |
| 933 |
|
| 934 |
**`plan_id` mapping:** |
| 935 |
|
| 936 |
| plan_id | Plan | |
| 937 |
|---------|------| |
| 938 |
| `1` | Free | |
| 939 |
| `15` | Infinite (annual) | |
| 940 |
| `16` | Growth (monthly) | |
| 941 |
| `17` | Infinite (monthly) | |
| 942 |
| `18` | Growth (annual) | |
| 943 |
|
| 944 |
**Quota block:** `is_over_quota()` returns true when `consumed_current_month_quota >= quota` on the Free plan. Optimization is rejected at the `Imagify\Optimization\File::optimize()` level. |
| 945 |
|
| 946 |
**Transient:** `imagify_user_cache` — TTL 5 minutes. Cleared on: API key change, account signup, `imagify_delete_user_data_cache` AJAX action. |
| 947 |
|
| 948 |
--- |
| 949 |
|
| 950 |
## 20. Roles & Capabilities |
| 951 |
|
| 952 |
| Capability | WP Capability | Defined In | |
| 953 |
|-----------|--------------|-----------| |
| 954 |
| `manage` | `manage_options` | `AbstractContext::get_capacity()` | |
| 955 |
| `optimize` | `upload_files` | `AbstractContext::get_capacity()` | |
| 956 |
| `manual-optimize` | `upload_files` | `AbstractContext::get_capacity()` | |
| 957 |
| `manual-restore` | `upload_files` | `AbstractContext::get_capacity()` | |
| 958 |
| `bulk-optimize` | `manage_options` | `AbstractContext::get_capacity()` | |
| 959 |
|
| 960 |
Filter: `option_page_capability_imagify` — override any capability mapping. |
| 961 |
|
| 962 |
--- |
| 963 |
|
| 964 |
## 21. Troubleshooting Tools — InternalStateList & Reset |
| 965 |
|
| 966 |
### `Imagify\Tools\InternalStateList` |
| 967 |
|
| 968 |
Single source of truth for all lockable/clearable state. Used by both `ResetInternalState` (admin tool) and `uninstall.php` (full cleanup). |
| 969 |
|
| 970 |
**Methods:** |
| 971 |
- `get_transients(): array` — list of transient keys to delete |
| 972 |
- `get_locked_transient_patterns(): array` — LIKE patterns for SQL bulk delete of locks |
| 973 |
- `get_action_scheduler_hooks(): array` — AS hook names to cancel all pending jobs |
| 974 |
|
| 975 |
### AJAX Action: `imagify_reset_internal_state` |
| 976 |
|
| 977 |
Requires `manage` capability. Steps: |
| 978 |
1. Delete all transients listed by `get_transients()` |
| 979 |
2. SQL `DELETE FROM wp_options WHERE option_name LIKE '_transient_%imagify...'` for each lock pattern |
| 980 |
3. `as_unschedule_all_actions('imagify_optimize_media')` |
| 981 |
4. `as_unschedule_all_actions('imagify_convert_next_gen')` |
| 982 |
5. Returns JSON `{success: true}` |
| 983 |
|
| 984 |
--- |
| 985 |
|
| 986 |
## 22. Error Handling Paths |
| 987 |
|
| 988 |
- **`WP_Error` throughout** — all `optimize()`, `restore()`, `File::optimize()` return `WP_Error` on failure, never throw exceptions. |
| 989 |
- **API errors** — `handle_response()` converts non-200 HTTP codes to `WP_Error` with code `'error {http_code}'` and the API's `detail` field as the message. |
| 990 |
- **File system errors** — `can_be_processed()` returns `WP_Error` with specific codes: `'file_not_exists'`, `'file_not_writable'`, `'dir_not_writable'`. |
| 991 |
- **Lock conflicts** — `optimize()` returns early (`false`) if `is_locked()` is truthy; does not return `WP_Error`. |
| 992 |
- **Quota exceeded** — returns `WP_Error` with code `'over_quota'` before hitting the API. |
| 993 |
- **Size exceeds 5 MB** — `is_exceeded()` triggers `WP_Error` with code `'file_too_big'`. |
| 994 |
|
| 995 |
--- |
| 996 |
|
| 997 |
## 23. Filesystem Operations & Paths |
| 998 |
|
| 999 |
All filesystem operations go through `Imagify_Filesystem` (wraps `WP_Filesystem`). Never uses `file_put_contents()` or `fopen()` directly. |
| 1000 |
|
| 1001 |
**Key path helpers:** |
| 1002 |
- `imagify_get_upload_basedir()` — returns `wp_upload_dir()['basedir']` with trailing slash |
| 1003 |
- `imagify_get_upload_baseurl()` — returns `wp_upload_dir()['baseurl']` |
| 1004 |
- `imagify_get_backup_dir()` — returns `{upload_basedir}backup/imagify/`; filterable via `imagify_backup_directory` |
| 1005 |
- `imagify_get_filesystem()` — returns `Imagify_Filesystem::get_instance()` |
| 1006 |
|
| 1007 |
**Next-gen sidecar paths:** |
| 1008 |
- WebP: `{original_path}.webp` (e.g. `photo.jpg.webp`) |
| 1009 |
- AVIF: `{original_path}.avif` (e.g. `photo.jpg.avif`) |
| 1010 |
- Note: sidecar files are never `.webp` or `.avif` as standalone extensions — the original extension is preserved as a prefix. |
| 1011 |
|
| 1012 |
--- |
| 1013 |
|
| 1014 |
*Imagify v2.2.8 · wp-media/imagify-plugin · Engineering Deep Dive* |
| 1015 |
|