PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / docs / imagify-technical-deep-dive.md

imagify-technical-deep-dive.md in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at docs/imagify-technical-deep-dive.md

1,015 lines 50.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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